C++20 Coroutine 协程是什么?
协程(Coroutines)是一种计算机程序组件,它允许在一个函数中暂停执行,并在稍后恢复执行。简而言之,协程是一种可以在函数执行期间暂停和恢复的控制流。
协程函数与普通函数有什么区别?
传统的函数在调用时开始执行,然后在返回时结束执行。而协程允许函数在执行过程中挂起,并在需要时恢复执行,而不是一次性执行完毕。
解决什么问题?
协程在编写异步代码时非常有用,因为它们可以使异步代码更加清晰、简洁,避免了回调地狱(Callback Hell)的问题。通过协程,可以以顺序的方式编写异步代码,而不必依赖于繁琐的回调函数或者复杂的线程同步机制。
碎语
协程在许多编程语言中都有支持,例如C++20引入了对协程的原生支持,Python的async/await
语法也是基于协程的概念实现的。在异步编程中,协程已经成为一种重要的编程范式。然而才在C++20引入了协程标准,可以不用依赖于其他协程库就实现程序在执行过程中暂停和恢复,而不会阻塞整个线程。有了协程使得编写异步代码更加简洁和易读。而官方和网上的案例相对比较局限,看着也不是那么通俗易懂,大部分案例也是嵌入到其他框架一起介绍的,这篇文章就以简单的几个小例子,更好的理解c++20的协程使用方法。
直接看母语 话不多说,我们直接先上一段封装好的小代码,先不管下面这个代码是啥,我们来研究如何使用就可以,后文再做详细的介绍。
下面代码命名保存为 coroutine.h,后面我们的案例中都包含该头文件。该头文件现已用在squick 框架中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #pragma once #include <coroutine> #include <functional> #include <time.h> template <typename T>class Coroutine {public : struct promise_type { T value_; auto initial_suspend () noexcept { return std::suspend_always{}; } auto final_suspend () noexcept { return std::suspend_always{}; } void unhandled_exception () noexcept {} Coroutine get_return_object () { return Coroutine{ handle_type::from_promise (*this ) }; } void return_void () {} template <std::convertible_to<T> From> std::suspend_always yield_value (From&& from) { value_ = std::forward<From>(from); return {}; } }; using handle_type = std::coroutine_handle<promise_type>; explicit Coroutine (handle_type handle) : coro_handle_(handle) { start_time_ = time (nullptr ); } ~Coroutine () {} handle_type GetHandle () const { return coro_handle_; } time_t GetStartTime () { return start_time_; } time_t start_time_ = 0 ;private : handle_type coro_handle_; };template <typename T>class Awaitable {public : bool await_ready () { return false ; } void await_suspend (std::coroutine_handle<> h) { coro_handle_ = h; handler_.operator ()(this ); } T await_resume () { return data_; } T data_; std::function< void (Awaitable<T>* awaitable)> handler_; std::coroutine_handle<> coro_handle_; };
例子1-创建协程 保存为demo1.cc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "coroutine.h" #include <iostream> Coroutine<int > MyCoroFunc () { std::cout << "hello coroutine!\n" ; co_return ; }int main () { auto co = MyCoroFunc (); auto h = co.GetHandle (); std::cout << "Coroutine address: " << h.address () << std::endl; h.resume (); if (h.done ()) { h.destroy (); std::cout << "Coroutine destroyed\n" ; } return 0 ; }
编译
1 g++ demo1.cc --std=c++20
输出
1 2 3 Coroutine address: 0x55c4d0609eb0 hello coroutine! Coroutine destroyed
通过该例子我们已经创建了个协程,并且去调用了MyCoroFunc函数。
例子2-获取协程函数中的返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include "coroutine.h" #include <iostream> Coroutine<int > MyCoroFunc () { std::cout << "MyCoroFunc: Coroutine run\n" ; co_yield 5 ; std::cout << "MyCoroFunc: Coroutine continue \n" ; co_return ; }int main () { auto co = MyCoroFunc (); auto h = co.GetHandle (); std::cout << "Coroutine is created, address: " << h.address () << std::endl; h.resume (); std::cout << "MyCoroFunc co_yield value: " << h.promise ().value_ << std::endl; h.resume (); if (h.done ()) { h.destroy (); std::cout << "Coroutine destroyed\n" ; } return 0 ; }
输出:
1 2 3 4 5 Coroutine is created, address: 0x56507223ceb0 MyCoroFunc: Coroutine run MyCoroFunc co_yield value: 5 MyCoroFunc: Coroutine continue Coroutine destroyed
例子3-如何在协程函数运行过程中异步获取外界传来的值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include "coroutine.h" #include <iostream> #include <string> class MyWork { struct Data { int id; std::string msg; }; public : void MyAwaitbleFuncHandler (Awaitable<Data> *a) { std::cout << "MyAwaitableFuncHandler, bind your corotine to manager\n" ; this ->awaitable_ = a; } Awaitable<Data> MyAwaitbleFunc () { Awaitable<Data> a; a.data_.id = 1234 ; a.handler_ = std::bind (&MyWork::MyAwaitbleFuncHandler, this , std::placeholders::_1); return a; } Coroutine<int > MyCoroFunc () { std::cout << "MyCoroFunc: Coroutine run\n" ; Data d = co_await MyAwaitbleFunc (); std::cout << "MyCoroFunc: MyAwaitableFunc return: " << d.msg << "\n" ; co_return ; } void DoWork () { auto co = MyCoroFunc (); auto h = co.GetHandle (); std::cout << "Coroutine is created, address: " << h.address () << std::endl; h.resume (); this ->awaitable_->data_.msg = "Hello awaitble" ; h.resume (); if (h.done ()) { h.destroy (); std::cout << "Coroutine destroyed\n" ; } } private : Awaitable<Data> *awaitable_ = nullptr ; };int main () { MyWork w; w.DoWork (); return 0 ; }
输出
1 2 3 4 5 Coroutine is created, address: 0x55cb276c4eb0 MyCoroFunc: Coroutine run MyAwaitableFuncHandler, bind your corotine to manager MyCoroFunc: MyAwaitableFunc return: Hello awaitble Coroutine destroyed
解释 看完了上面例子,看不懂也没关系,我在这里做一些简单的概念。
C++20引入了协程(Coroutines)的支持,这是一项重要的新功能,使得异步编程变得更加容易和直观。协程提供了一种在函数内部暂停和恢复执行的机制,从而使得编写异步代码更加简洁、可读性更高。
以下是C++20协程的一些关键概念和特性:
协程关键字: C++20引入了co_await
、co_yield
、co_return
等新的关键字,用于在协程内部进行挂起、恢复和返回操作。
协程函数: 使用co_return
可以在协程函数中返回值,并在此处暂停协程的执行。协程函数可以返回一个期待值,而不是立即返回,从而使得在异步操作完成后再继续执行。
协程生成器(Coroutine Generator): 通过co_yield
关键字,可以在协程内部产生值并暂停执行。这种机制非常适合生成序列或流式数据。
协程状态机: 编译器会将协程函数转换成状态机的形式,以便在暂停和恢复时保存和恢复执行上下文。
std::coroutine_handle
: 这是一个轻量级的句柄,用于管理协程的生命周期和执行状态。它可以用来手动控制协程的执行,例如恢复、挂起和销毁。
协程的异步编程: 协程可以与异步任务一起使用,例如与Future、Promise、I/O操作等结合,从而实现高效的异步编程模式,而无需显式地使用回调函数或复杂的线程管理。
协程的异常处理: 协程内部的异常可以通过co_await
或co_yield
传递给调用方进行处理,也可以通过协程的promise_type
自定义异常处理逻辑。
协程库: C++20标准库提供了与协程相关的头文件<coroutine>
,其中包含了一些与协程相关的类和函数,例如std::coroutine_traits
、std::suspend_always
、std::suspend_never
等。
来看看先前例子中所写的头文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #pragma once #include <coroutine> #include <functional> #include <time.h> template <typename T>class Coroutine { public : struct promise_type { T value_; auto initial_suspend () noexcept { return std::suspend_always{}; } auto final_suspend () noexcept { return std::suspend_always{}; } void unhandled_exception () noexcept {} Coroutine get_return_object () { return Coroutine{ handle_type::from_promise (*this ) }; } void return_void () {} template <std::convertible_to<T> From> std::suspend_always yield_value (From&& from) { value_ = std::forward<From>(from); return {}; } }; using handle_type = std::coroutine_handle<promise_type>; explicit Coroutine (handle_type handle) : coro_handle_(handle) { start_time_ = time (nullptr ); } ~Coroutine () {} handle_type GetHandle () const { return coro_handle_; } time_t GetStartTime () { return start_time_; } time_t start_time_ = 0 ;private : handle_type coro_handle_; };template <typename T>class Awaitable { public : bool await_ready () { return false ; } void await_suspend (std::coroutine_handle<> h) { coro_handle_ = h; handler_.operator ()(this ); } T await_resume () { return data_; } T data_; std::function< void (Awaitable<T>* awaitable)> handler_; std::coroutine_handle<> coro_handle_; };
就先介绍到这里了,学会了以上基本没啥大问题,协程的内存管理,可以通过智能指针来管理,以上例子中都是手动释放内存的。对协程的内存管理也是一门学问,看自己的框架怎么设计喽。比如下面这几个伪代码是利用上面我介绍的例子实际运用到http服务器中的。
https://github.com/pwnsky/squick/blob/main/src/tutorial/t5_http/http_module.cc
1 2 3 4 5 6 Coroutine<bool > HttpModule::ClientAsyncGet (std::shared_ptr<HttpRequest> req) { std::cout << "You use await to async request another server\n" ; auto data = co_await m_http_client_->CoGet ("http://www.bilibili.com" ); m_http_server_->ResponseMsg (req, data.content, WebStatus::WEB_OK); co_return ; }
大家有兴趣可以去看看我写的squick 游戏框架。
参考 https://en.cppreference.com/w/cpp/language/coroutines
https://chat.openai.com/chat