在上一篇中,介绍了 lewissbaker 的三篇文章,实际上覆盖了 C++ 的无栈协程的实现原理,这里介绍几个常见的协程库的使用。
cppcoro
https://github.com/lewissbaker/cppcoro
修改既有代码
io
锁
协程调度的核心思想是避免阻塞线程,而 std::mutex 的同步原语通常会导致线程阻塞。因此,需要用 cppcoro::async_mutex
进行替换。
注意 std::unique_lock 等这样的锁结构并不是同步原语。它的实现中,没有包含线程上下文切换的部分。而相关的逻辑,实际上是通过调用 std::muex 的 lock 和 unlock 函数来实现的。
尽管如此,cppcoro 中还是提供了诸如 async_mutex_lock 等结构。但这样的结构是为了实现协程而服务的。可以看对应的章节。
主要数据结构
task
一段可以被异步计算的逻辑,它是被 lazily 执行的。当 await 它的时候,它开始执行。
下面给了一个读写文件统计行数的例子,用到了:
co_await cppcoro::read_only_file::open(path);
co_await file.read(offset, buffer, sizeof(buffer));
一个 co_await 的函数必须用到 co_await 或者 co_return,但未必会用到 co_yield。返回值的类型为 task<T>
。
当一个返回 task
当这个 task
task
如果一个协程已经计算得到了结果,那么 await 它不会导致挂起,而是直接返回结果。
如果在 await 之前,那个 task 对象就已经被销毁了,那么这个协程就不会被执行,并且析构函数会销毁被捕获的参数,并且释放被 coroutine frame 使用的内存。
shared_task
我理解是单生产者多消费者模式。
generator / recursive_generator
generator
用法如下所示,但是其中不能使用 co_await,也就是说必须同步地去计算并产生这些值。
1 | cppcoro::generator<const std::uint64_t> fibonacci() |
将 fmap 应用到 recursive_generator
recursive_generator
1 | // Lists the immediate contents of a directory. |
async_generator
如下所示,可以 co_await 了。
1 | cppcoro::async_generator<int> ticker(int count, threadpool& tp) |
async_mutex
co_wait 这个 async_mutex 会挂起这个协程,直到这个 mutex 被释放。
这里和 std::mutex 不同的是,async_mutex 是 lock free 的。lock 它并不会 block 当前线程,而只是挂起当前的 coroutine。
如下所示,lock_async 调用会返回一个 async_mutex_lock_operation。这个 async_mutex_lock_operation 必须要被 co_await。co_await 的返回值的类型是 void。
scoped_lock_async 调用会返回一个 async_mutex_scoped_lock_operation。同样它也需要被 co_await。co_await 返回值的类型是 async_mutex_lock。async_mutex_lock 在析构的时候会自动调用持有的 mutex 的 unlock 方法。
1 | // <cppcoro/async_mutex.hpp> |
sync_wait
类似于 rust tokio 的 block_on
。
when_all_ready/when_all
类似于 rust tokio 的 futures::join!
。两者比较,则 when_all_ready 的功能更强大:
- when_all_ready 可以等所有的协程都成功或者失败地跑完,然后分别获取每个操作的结果,它本身不会抛出异常。而 when_all 中,只要有一个 task 抛出异常,那么整个 task 就会抛出异常然后失败。
- when_all_ready 的返回值是 when_all_task,需要调用 result() 获取结果。而 when_all 返回的是 void 或者 vector。