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++ Primer 标准库vector示例详解

《C++Primer标准库vector示例详解》该文章主要介绍了C++标准库中的vector类型,包括其定义、初始化、成员函数以及常见操作,文章详细解释了如何使用vector来存储和操作对象集合,... 目录3.3标准库Vector定义和初始化vector对象通列表初始化vector对象创建指定数量的元素值

基于Python实现多语言朗读与单词选择测验

《基于Python实现多语言朗读与单词选择测验》在数字化教育日益普及的今天,开发一款能够支持多语言朗读和单词选择测验的程序,对于语言学习者来说无疑是一个巨大的福音,下面我们就来用Python实现一个这... 目录一、项目概述二、环境准备三、实现朗读功能四、实现单词选择测验五、创建图形用户界面六、运行程序七、

C++实现回文串判断的两种高效方法

《C++实现回文串判断的两种高效方法》文章介绍了两种判断回文串的方法:解法一通过创建新字符串来处理,解法二在原字符串上直接筛选判断,两种方法都使用了双指针法,文中通过代码示例讲解的非常详细,需要的朋友... 目录一、问题描述示例二、解法一:将字母数字连接到新的 string思路代码实现代码解释复杂度分析三、

前端知识点之Javascript选择输入框confirm用法

《前端知识点之Javascript选择输入框confirm用法》:本文主要介绍JavaScript中的confirm方法的基本用法、功能特点、注意事项及常见用途,文中通过代码介绍的非常详细,对大家... 目录1. 基本用法2. 功能特点①阻塞行为:confirm 对话框会阻塞脚本的执行,直到用户作出选择。②

C++一个数组赋值给另一个数组方式

《C++一个数组赋值给另一个数组方式》文章介绍了三种在C++中将一个数组赋值给另一个数组的方法:使用循环逐个元素赋值、使用标准库函数std::copy或std::memcpy以及使用标准库容器,每种方... 目录C++一个数组赋值给另一个数组循环遍历赋值使用标准库中的函数 std::copy 或 std::

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

c++中std::placeholders的使用方法

《c++中std::placeholders的使用方法》std::placeholders是C++标准库中的一个工具,用于在函数对象绑定时创建占位符,本文就来详细的介绍一下,具有一定的参考价值,感兴... 目录1. 基本概念2. 使用场景3. 示例示例 1:部分参数绑定示例 2:参数重排序4. 注意事项5.