C++对我来说简直就是星辰大海,为了避免翻船,我选择从小河沟出发

2023-12-04 00:32

本文主要是介绍C++对我来说简直就是星辰大海,为了避免翻船,我选择从小河沟出发,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 进程 vs 线程 vs 协程
  • 同步 vs 异步
  • 阻塞 vs 非阻塞
  • 协程学习
    • 消费者-生产者
    • 自己想个例子
      • 常规写法
      • 进阶写法
      • 协程写法
  • 总结

你学的越多,不懂的东西反而越多~

前言

以前觉得 C++ 并没有什么复杂的,不就是 C 语言加上类定义、模板、容器、算法函数这些就可以了吗,只要我不用,它就难不倒我,用到了查查文档也就搞定了,真的是年少轻狂啊。

随着学习的深入渐渐发现,即使抛开那些算法函数、那些冗长的模板,单单是 C++ 核心的概念和类型就够喝上好几壶的,随便罗列几个,像 std::furnitruestd::memory_orderstd::packaged_task 等等这些,之前都没听说过,特别是C++20的协程,到现在还是一头雾水。

C++ 缺少了 C 语言的纯粹,总是喜欢在编译时加点料,但是这个协程加的料超多,一时间还有点接受不了。

不过第一次听说协程这个词是在 Lua 中,全称被叫做协同程序,记得没错是在 《Lua程序设计》这本书中看到的,里面专门有一章是讲coroutine的,并且在 Lua 中定义和使用协程很方便,所以决定先复习一下 Lua 中的协程,然后对比着 C++的协程来进行拓展学习。

进程 vs 线程 vs 协程

这三者常常被拿来比较,而引入多进程、多线程、多协程有一个简单而纯粹的目的,那就是榨干CPU,不过这三者侧重还有所不同。

进程是资源分配最小单位,每个进程都有独立的地址空间,来维护代码段、堆栈段和数据段,但是创建和切换进程的开销较大,可以在多台物理机和多核CPU上提高效率,依靠管道(pipe)、命名管道(named pipe/FIFO)、信号量(semophore)、消息队列(message queue)、信号(sinal)、共享内存(shared memory)、套接字(socket)、全双工管道等途径来进行通信。

线程是任务调度和执行的最小单位,没有独立的地址空间,但有独立的运行栈和程序计数器(PC),创建和切换线程的开销相比进程来说要小得多,线程之间通信更加方便,除了可以使用进程间通信的方式,还可以简单地通过共享全局变量,静态变量等进行通信,但是需要锁机制、信号量机制、信号机制来控制线程间互斥。

协程这个概念就比较迷了,其实它不像多进程和多线程那样可以在多核机器上提供并行的能力,而是侧重于相互协作共同完成某个任务,同一个线程中可以启动多个协程,但这些协程同一时刻只能有一个在运行。

协程其实可以看成是一个可以被随时停止和唤醒的函数,使用协程是为了在用户层面来控制调用逻辑,对比于多线程程序的线程调度完全看操作系统的心情的处境,多协程的程序就比较自主了,可以由开发者来控制函数执行顺序。

还有一个特性很重要,就是使用协程可以实现用“同步”的方式来写“异步”的代码,这一点不理解没关系,以后会慢慢明白的。说到这,不得不说一下关于同步和异步、阻塞和非阻塞这几个概念,它们常常被大家混在一起来说,实际上只是从不同维度来描述了一件事情,下面简单叙述下。

同步 vs 异步

同步和异步指的是消息通信的机制,或者说得到结果的方式。

  • 同步:调用函数后就能返回想要的结果,有点像去食堂买饭,自己去食堂付完钱(调用),饭(结果)就可以被拿回来了,这就是同步调用的方式,与返回结果的时间长短无关,得到结果之后直接执行后面的逻辑(吃饭)就可以了,所以同步的逻辑是最好写的。

  • 异步:调用函数后并不能直接得到想要的结果,需要通过回调或者其他消息来通知,这就有点像定外卖了,打开APP选好饭菜输入地址(注册回调),开始付钱(调用),此时并不能直接得到饭(结果),而是一段时间之后,有外卖小哥将饭(结果)给你送来,这时才能执行后面的逻辑(吃饭)。

总结来说,需要自己取结果的就是同步,依靠别人送结果的就是异步。

阻塞 vs 非阻塞

阻塞和非阻塞指的是程序在等待调用结果时的状态,强调在获得结果之前的表现。

  • 阻塞:调用函数后由于不满足某种条件(比如读socket但是没有数据)被挂起,当条件满足(socket来数据了)时被唤醒,并将结果返回。

  • 非阻塞:调用函数后如果不满足指定条件(比如读socket但是没有数据)不挂起,而是返回一个表示没有取到结果的值,你可以按照某种间隔再次调用函数,直到取到结果为止,当然你也可以调用一次就结束了。

总结来说,不满足条件时调用方被挂起就是阻塞调用,否则就是非阻塞调用。

协程学习

C++的协程是暂时学不明白了,为了不翻车,我还是从熟悉的 Lua 入手,来举例说明什么是协程?有什么用?为什么这样用?弄明白以后再慢慢用 C++ 来实现相同的目的,毕竟 C++ 这一块需要实现的内容也有点多。

消费者-生产者

提到 Lua 的协程就会想到 “消费者-生产者”的例子,网上关于这个的实现有特别多的版本,整体上来说大同小异,基本上都是 《C++程序设计》这本书中的内容,但是这一部分我看了很多遍,感觉这个例子并不太好。

function receive(prod)  -- 激活协同程序local status,value = coroutine.resume(prod)return value
endfunction send(x)  -- 挂起协同程序coroutine.yield(x)
endfunction producer()  -- 生产者return coroutine.create(  -- 创建协同程序function()while true dolocal x = io.read()  -- 产生新值send(x)endend)
endfunction filter(prod)  -- 过滤器return coroutine.create(  -- 创建协同程序function()for line = 1, math.huge dolocal x = receive(prod)  -- 激活协同程序来获取新值x = string.format("%5d %s",line , x )  -- 过滤规则send(x)  -- 挂起激活程序endend)
endfunction consumer(prod)while true dolocal x = receive(prod) -- 获取新值io.write(x, "\n")       -- 消费新值end
endp= producer()  -- 初始化生产者
f = filter(p)  -- 初始化过滤器
consumer(f)    -- 初始化消费者并启动程序

这就是一个消费者驱动的模型,首先由启动消费者,然后调用生产者来生产资源,接着消费者消耗掉新的资源,再控制生产者生产新的资源,以此方式循环往复,其实就是下面代码的复杂化:

function consumer_producer()while true dolocal x = io.read()  -- 产生新值io.write(x, "\n")    -- 消费新值end
endconsumer_producer() -- 启动生产者消费者

这个例子以我现在的菜鸟水平来看没啥用,但是有一点比较好,就是展示了可以用协程来控制程序执行顺序的强大功能,只是这个消费者和生产者强耦合的设计实在是看不明白。

自己想个例子

既然他们的例子我都不喜欢,那我就自己想一个,叮铃铃!下面我收到了一个新的需求:

计算1+2+3+4+5+6+7+8+9+10的和,然后等待5秒钟后,将结果显示在控制台上。

乍一听,这个需求太简单了吧,没有一点难度,其实不然,其中蕴含着大量玄机,简直就是一个万能句式:

做一件事情A,然后等待某件事发生,再做一件事情B(可能与A相关)

仔细想想,这样的“句式”在开发中,生活中是不是经常出现?

  1. 下载电影,下载完成后,播放电影
  2. 开始加载场景,加载完成后,隐藏加载进度条
  3. 发送一个请求,收到回复时,将回复结果显示出来

看了吧,现实中有很多这类需求,我们接下来尝试着实现一下

常规写法

-- lua 没有 sleep 函数,使用while循环模拟
function sleep(n)local t = os.clock()while os.clock() - t <= n do end
endfunction task_method_1()print(string.format("program start at %s", os.date("%H:%M:%S")))-- 求和local sum = 0;for i=1,10 dosum = sum + i;end-- 等待sleep(5);-- 展示print(string.format("program end at %s and sum = %d", os.date("%H:%M:%S"), sum))
endfunction main1()task_method_1()
endmain1()

代码很简单,为了看起来更连贯这里就不分段展示了,首先模拟一个 sleep 函数,然后实现 task_method_1 函数来完成原始需求——求和、等待、展示,最后通过主函数来调用就可以了。

运行结果如下:

program start at 01:30:27
program end at 01:30:32 and sum = 55

进阶写法

看了上面的代码有没有发现什么问题?这是一种同步的实现方式,整个程序在中间等待的5秒钟什么都不能做,必须等倒计时结束才能做后面的事情,这要是购物APP点了5秒没反应就直接X掉了,这可是赤果果的金钱损失啊,绝不能让这种事情发生。

怎么办呢?我确实需要5秒钟的处理时间,但是又不能让用户卡在那,我可以显示一个进度条,进度一直再变化,用户就不会以为程序卡死了,如果进度走的比较慢,他可能以为手机老旧该换了,没准还促进了手机的销量呢!

顺着这个思路写出了下面这种实现,这是一种异步的实现方式,通过回调函数来通知最终要显示的结果。

function task_method_2()print(string.format("program start at %s", os.date("%H:%M:%S")))-- 求和local sum = 0;for i=1,10 dosum = sum + i;end-- 注册回调函数,进行等待add_callback(5, call_back_print, sum)
endfunction call_back_print(data)--展示结果print(string.format("program end at %s and sum = %d", os.date("%H:%M:%S"), data))
endfunction add_callback(inteval, func, data)interval_time = intevalcall_back = funcmsg_data = data
endfunction main2()local t0 = os.clock();local t = t0;task_method_2()while true dolocal now = os.clock()if now - t >= 1 thenprint(string.format("program run %f seconds", now - t0))t = now;if interval_time and call_back and now - t0 >= interval_time thencall_back(msg_data)break;endendend
endmain2()

在函数 task_method_2 中计算完求和的结果,并没有等待,而是通过 add_callback 函数注册了等待时间、回调函数、以及回调展示的结果,然后直接返回了调用方,调用主函数 main2 中计算这时间差并展示进度,等倒计时一结束就执行回调函数,进而展示出结果。

运行结果如下,通过打印信息展示处理进度条:

program start at 01:44:56
program run 1.000000 seconds
program run 2.001000 seconds
program run 3.001000 seconds
program run 4.001000 seconds
program run 5.001000 seconds
program end at 01:45:01 and sum = 55

协程写法

卡顿的问题解决了,但是添加了一大堆额外的注册和回调函数,有些麻烦啊,怎么把它们去掉呢?

终于等到协程出场了,同步调用很卡、异步回调很烦,那么协程可以实现用“同步”的方式来写“异步”的代码,既不卡也不烦,下面来看一下实现。

function task_method_3()print(string.format("program start at %s", os.date("%H:%M:%S")))-- 求和local sum = 0;for i=1,10 dosum = sum + i;end-- 等待coroutine.yield(5);-- 展示print(string.format("program end at %s and sum = %d", os.date("%H:%M:%S"), sum))
endfunction main3()local t0 = os.clock();local t = t0;local co = coroutine.create(task_method_3)local status, interval = coroutine.resume(co)while true dolocal now = os.clock()if now - t >= 1 thenprint(string.format("program run %f seconds", now - t0))t = now;if now - t0 >= interval thencoroutine.resume(co)break;endendend
endmain3()

对比 task_method_3task_method_1 函数,只是将 sleep 函数换成了 coroutine.yield(5),整个需求函数很紧凑。

程序运行逻辑是这样的,先将 task_method_3 函数包装成协程 co,然后启动 co 执行求和逻辑,执行到 coroutine.yield(5); 这句,协程被暂停并将5返回,主函数 main3 中收到返回值5后开始计时并展示进度值,直到5秒等待期结束再次唤醒协程 cocoroutine.yield(5); 后面的代码继续执行,完成最后的展示需求。

运行结果如下:

program start at 01:50:59
program run 1.000000 seconds
program run 2.000000 seconds
program run 3.000000 seconds
program run 4.000000 seconds
program run 5.000000 seconds
program end at 01:51:04 and sum = 55

总结

  • 多进程/多线程的引入并不是总能降低任务消耗的时间,还要考虑到进程/线程切换的消耗问题,参考Redis实现
  • 多协程的引入本质上是为了更好的控制程序运行的逻辑,虽然它往往也能带来效率上的提升
  • coroutine.yield 是协程的中核心函数,主动让出CPU,如果协程不自己挂起,外部无法干预
  • 知识的迁移是一项重要的技能,下一步要用C++协程来实现这个需求啦,边学边写喽

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

拨开那一片云,是你未曾实现的梦想,岁月流转,梦想在变,有些事不得不放弃坚守(固执),珍惜眼前的一切,迎接明天的朝阳~

这篇关于C++对我来说简直就是星辰大海,为了避免翻船,我选择从小河沟出发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/451442

相关文章

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

Python 中 requests 与 aiohttp 在实际项目中的选择策略详解

《Python中requests与aiohttp在实际项目中的选择策略详解》本文主要介绍了Python爬虫开发中常用的两个库requests和aiohttp的使用方法及其区别,通过实际项目案... 目录一、requests 库二、aiohttp 库三、requests 和 aiohttp 的比较四、requ

el-select下拉选择缓存的实现

《el-select下拉选择缓存的实现》本文主要介绍了在使用el-select实现下拉选择缓存时遇到的问题及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录项目场景:问题描述解决方案:项目场景:从左侧列表中选取字段填入右侧下拉多选框,用户可以对右侧

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

如何选择适合孤独症兄妹的学校?

在探索适合孤独症儿童教育的道路上,每一位家长都面临着前所未有的挑战与抉择。当这份责任落在拥有孤独症兄妹的家庭肩上时,选择一所能够同时满足两个孩子特殊需求的学校,更显得尤为关键。本文将探讨如何为这样的家庭做出明智的选择,并介绍星贝育园自闭症儿童寄宿制学校作为一个值得考虑的选项。 理解孤独症儿童的独特性 孤独症,这一复杂的神经发育障碍,影响着儿童的社交互动、沟通能力以及行为模式。对于拥有孤独症兄

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�