c++协程初探

简介

介绍协程之前,先来复习一下进程和线程。

  • 进程:应用程序的启动实例,是计算机资源分配的最小单位,进程拥有代码和打开的文件资源、数据资源、独立的内存空间。
  • 线程:从属于进程,是程序的实际执行者。是计算机调度和执行的最小单位。一个进程至少包含一个线程,为主线程,也可以有更多的子线程,子线程共享进程的文件和数据。线程拥有自己的栈空间。

它们在操作系统中的关系如图:

在这里插入图片描述

通常,应用程序是采用单进程多线程的方式,这样既能使用较小的系统资源,又能把占用的资源使用率最大化。

多线程允许程序充分利用多核cpu的优势,实现程序真正的并行运行。

但多线程也存在一定的问题:

  • 多线程共享进程的数据,当多个线程可能会同时读写一段数据时,需要使用互斥保证数据一致性
  • 线程在不同的cpu核心上调度时,会涉及到运行状态的保存和恢复、上下文切换等开销
  • 频繁地线程启动和停止会影响程序执行效率
  • 每个线程都会有一个独立的调用栈,线程数量较多时占用较大内存
  • 操作系统会有线程数据的上限,超过上限就不能再创建新线程了

为了提高程序的执行效率,往往需要使用异步调用。

  • 由调用方盲目主动问询的方式是同步调用,由被调用方主动通知调用方任务已完成的方式。
  • 这样避免了同步调用方式中的程序等待,在大并发中效率会有大幅提升。

但是异步编程较为复杂,容易出错且不易调试。

那么问题来了,有没有既能利用多线程和异步的优势,又能避免它们随之而来的缺点的技术呢?

没错,是协程。

协程与子例程(函数)一样,协程(coroutine)也是一种程序组件。协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。

由于协程的理念与之前经常使用技术不同,有必要详细讲解一下。

协程

子例程(函数),在所有语言中都是层级调用。比如A调用B,B在执行过程中又调用了C,C执行完毕返回B,B执行完毕返回A,最后是A执行完毕。

很显然,函数调用是通过栈实现的。而一个线程其实就是在执行一系列的函数。

函数调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是函数,但执行过程中,在函数内部可中断,然后转而执行别的函数,在适当的时候再返回来接着执行。

注意,在一个函数中中断,去执行其他函数,不是通过函数调用,这有点类似CPU的中断。

比如有两个协程,A执行一些操作,B执行另一些操作。从执行效果上看,A的操作执行一部分,接着是B执行一部分,然后又回到A执行,再回去B执行。从效果上看,运行结果又有点像多线程。

相对于多线程,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),协程的优点是:

  • 执行效率极高。因为是函数切换,而不是线程调度切换,不会有线程调度的开销。在实际应用中,需要的线程越多,协程的高效越明显。
  • 不需要锁。多线程同步一直是个影响效率的重要因素,也是经常出bug的地方。而协程是个单个线程中,共享资源不需要锁,只需要判断一下状态,效率极高。

这里,有一个问题,多个协程是在单个线程中运行,单个运行同时只能运行在一个核心上,如何利用多核心呢?

这是协程的缺点之一,即不能利用多核资源。因为协程本质上是个子例程(函数)。

另一个缺点是,进行阻塞操作时(如IO),会阻塞掉整个程序。

这两个缺点其实也容易解决:

最简单的方法是使用多线程,同时每个线程中应用协程,这样既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

到这里了,可以打个比方。

在现实的世界中,每个人,每件东西,其实都是在并发地运行着。你在看这篇文章时,你的朋友在高速路上奔驰,你家的小猫咪在睡觉,你公司楼下的树木在进行光合作用…

而协程,就是要做到每个子例程独立地做自己的事情,而各个协程之间并发地运行。

使用Go的同学,包括之前可能使用过Erlang的同学,对这点应该比较熟悉了。因为它们天生支持并发,协程自然不在话下。

c++对协程的支持

c++对协程的支持不如Go、Lua等语言,但也有一些支持协程的开源库可使用,如boost::coroutine、libgo等。

c++20中已经支持协程,使用了 co_await、co_yield、co_return任何一个关键字的函数都是协程。

如下代码使用协程生成数字:

generator<int> get_integers( int start=0, int step=1 ) {
  for (int current=start; true; current+= step)
    co_yield current;
}
  • co_yield会挂起函数执行,并在generator中保存当前状态
  • 通过generator返回current的值
  • 数字返回后可循环执行

co_await 可在协程之间切换。如果协程需要另一协程的处理结果,只需要co_await它。待它准备好后会立即处理。如果未准备好,协程会等待。

std::future<std::expected<std::string>> load_data( std::string resource )
{
  auto handle = co_await open_resouce(resource);
  while( auto line = co_await read_line(handle)) {
    if (std::optional<std::string> r = parse_data_from_line( line ))
       co_return *r;
  }
  co_return std::unexpected( resource_lacks_data(resource) );
}
  • load_data是一个协程,当打开指定的资源时,且解析到所请求数据的位置后,它将生成std :: future并返回。
  • open_resource和read_lines可能是异步协程,它们会打开文件并读取数据。
  • co_await将load_data的暂停状态和就绪状态与其进度联系起来。

作为用户空间类型之上的最少语言功能集实现的,C++协程的使用非常灵活。

在以后的使用中分享。

小结

  • 线程和协程推荐在IO密集型的任务(比如网络调用)中使用,而在CPU密集型的任务中,表现较差。
  • 对于CPU密集型的任务,则需要多个线程,绕开GIL的限制,利用所有可用的CPU核心,提高效率。
  • 所以大并发下的最佳实践就是多线程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能,如下图:

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++协程是一种轻量级的线程,可以在单个线程中实现多个协程C++20引入了协程库,其中包括协程原语和一些实用程序,使协程的使用变得更加容易。下面是一个简单的C++协程实现demo: ```c++ #include <iostream> #include <coroutine> struct Generator { struct promise_type { int current_value; Generator get_return_object() { return Generator(std::coroutine_handle<promise_type>::from_promise(*this)); } auto initial_suspend() { return std::suspend_always{}; } auto final_suspend() noexcept { return std::suspend_always{}; } void unhandled_exception() {} void return_void() {} auto yield_value(int value) { current_value = value; return std::suspend_always{}; } }; std::coroutine_handle<promise_type> coroutine; Generator(std::coroutine_handle<promise_type> h) : coroutine(h) {} ~Generator() { if (coroutine) coroutine.destroy(); } int get() { coroutine.resume(); return coroutine.promise().current_value; } }; Generator counter(int begin, int end) { for (int i = begin; i <= end; ++i) co_yield i; } int main() { auto gen = counter(1, 5); while (auto value = gen.get()) std::cout << value << ' '; } ``` 这个demo中,我们定义了一个生成器(Generator)结构体和一个promise_type结构体。Generator结构体包含一个协程句柄,可以通过该句柄来操作协程。promise_type结构体定义了生成器的类型,并提供了返回对象,挂起,终止等方法。在counter函数中,我们使用co_yield关键字来挂起当前协程并返回值。在主函数中,我们使用while循环来不断调用协程的get方法来获取生成的值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值