当前位置: 首页 > news >正文

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
end

function send(x)  -- 挂起协同程序
  coroutine.yield(x)
end

function producer()  -- 生产者
  return coroutine.create(  -- 创建协同程序
    function()
          while true do
              local x = io.read()  -- 产生新值
              send(x)
          end
      end
  )
end

function filter(prod)  -- 过滤器
  return coroutine.create(  -- 创建协同程序
      function()
          for line = 1, math.huge do
              local x = receive(prod)  -- 激活协同程序来获取新值
              x = string.format("%5d %s",line , x )  -- 过滤规则
              send(x)  -- 挂起激活程序
          end
      end
  )
end

function consumer(prod)
  while true do
      local x = receive(prod) -- 获取新值
      io.write(x, "\n")       -- 消费新值
  end
end

p= producer()  -- 初始化生产者
f = filter(p)  -- 初始化过滤器
consumer(f)    -- 初始化消费者并启动程序

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

function consumer_producer()
  while true do
      local x = io.read()  -- 产生新值
      io.write(x, "\n")    -- 消费新值
  end
end

consumer_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
end

function task_method_1()
    print(string.format("program start at %s", os.date("%H:%M:%S")))

    -- 求和
    local sum = 0;
    for i=1,10 do
        sum = sum + i;
    end

    -- 等待
    sleep(5);

    -- 展示
    print(string.format("program end at %s and sum = %d", os.date("%H:%M:%S"), sum))
end

function main1()
    task_method_1()
end

main1()

代码很简单,为了看起来更连贯这里就不分段展示了,首先模拟一个 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 do
        sum = sum + i;
    end

    -- 注册回调函数,进行等待
    add_callback(5, call_back_print, sum)
end

function call_back_print(data)
    --展示结果
    print(string.format("program end at %s and sum = %d", os.date("%H:%M:%S"), data))
end

function add_callback(inteval, func, data)
    interval_time = inteval
    call_back = func
    msg_data = data
end

function main2()
    local t0 = os.clock();
    local t = t0;

    task_method_2()

    while true do
        local now = os.clock()
        if now - t >= 1 then
            print(string.format("program run %f seconds", now - t0))
            t = now;

            if interval_time and call_back and now - t0 >= interval_time then
                call_back(msg_data)
                break;
            end
        end
    end
end

main2()

在函数 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 do
        sum = sum + i;
    end

    -- 等待
    coroutine.yield(5);

    -- 展示
    print(string.format("program end at %s and sum = %d", os.date("%H:%M:%S"), sum))
end


function main3()
    local t0 = os.clock();
    local t = t0;

    local co = coroutine.create(task_method_3)
    local status, interval = coroutine.resume(co)

    while true do
        local now = os.clock()
        if now - t >= 1 then
            print(string.format("program run %f seconds", now - t0))
            t = now;

            if now - t0 >= interval then
                coroutine.resume(co)
                break;
            end
        end
    end
end

main3()

对比 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++协程来实现这个需求啦,边学边写喽

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

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

相关文章:

  • cpplint中filter参数的每个可选项的含义
  • 手把手搭建一个redis集群
  • 换个角度来看看C++中的左值、右值、左值引用、右值引用
  • C/C++中的数据类型转换()/static_cast/dynamic_cast/const_cast/reinterpret_cast
  • C++11中std::move和std::forward到底干了啥
  • 使用box2dweb做一个下落的小球,宝宝玩的不亦乐乎
  • C++中使用std::sort自定义排序规则时要注意的崩溃问题
  • 从一个小题中的应用来体会下std::tie的便利之处
  • Floyd-Warshall——仅用4行代码就能解决多源最短路径问题的算法
  • Dijkstra——通过不断松弛来解决单源最短路径问题的算法
  • C++11中的std::atomic保证的原子性是什么
  • .bat批处理(十):从路径字符串中截取盘符、文件名、后缀名等信息
  • linux环境下从路径字符串中截取目录和文件名信息
  • MD5是用来加密的吗?BCrypt又是什么呢
  • 树的带权路径长度和哈夫曼树
  • 4. 路由到控制器 - Laravel从零开始教程
  • Apache Spark Streaming 使用实例
  • CSS 三角实现
  • IP路由与转发
  • JavaScript设计模式系列一:工厂模式
  • JWT究竟是什么呢?
  • mysql innodb 索引使用指南
  • open-falcon 开发笔记(一):从零开始搭建虚拟服务器和监测环境
  • Python学习之路13-记分
  • ReactNativeweexDeviceOne对比
  • 对JS继承的一点思考
  • 多线程事务回滚
  • 高程读书笔记 第六章 面向对象程序设计
  • 罗辑思维在全链路压测方面的实践和工作笔记
  • 如何设计一个微型分布式架构?
  • 算法---两个栈实现一个队列
  • 正则表达式小结
  • Redis4.x新特性 -- 萌萌的MEMORY DOCTOR
  • #QT(串口助手-界面)
  • $ git push -u origin master 推送到远程库出错
  • (C++)栈的链式存储结构(出栈、入栈、判空、遍历、销毁)(数据结构与算法)
  • (ctrl.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (poj1.2.1)1970(筛选法模拟)
  • (办公)springboot配置aop处理请求.
  • (二) Windows 下 Sublime Text 3 安装离线插件 Anaconda
  • (心得)获取一个数二进制序列中所有的偶数位和奇数位, 分别输出二进制序列。
  • (一)80c52学习之旅-起始篇
  • (转)linux 命令大全
  • (转)Oracle 9i 数据库设计指引全集(1)
  • (转贴)用VML开发工作流设计器 UCML.NET工作流管理系统
  • .libPaths()设置包加载目录
  • .NET CF命令行调试器MDbg入门(四) Attaching to Processes
  • .NET Compact Framework 3.5 支持 WCF 的子集
  • .NET(C#、VB)APP开发——Smobiler平台控件介绍:Bluetooth组件
  • @configuration注解_2w字长文给你讲透了配置类为什么要添加 @Configuration注解
  • @GetMapping和@RequestMapping的区别
  • @LoadBalanced 和 @RefreshScope 同时使用,负载均衡失效分析
  • [autojs]autojs开关按钮的简单使用
  • [HEOI2013]ALO