C++20 特性 協(xié)程 Coroutines(1)
我們先來介紹一下什么是協(xié)程.
一、協(xié)程簡單介紹
協(xié)程和普通的函數(shù) 其實差不多. 不過這個 "函數(shù)" 能夠暫停自己, 也能夠被別人恢復.
普通的函數(shù)調用, 函數(shù)運行完返回一個值, 結束.
協(xié)程可以運行到一半, 返回一個值, 并且保留上下文. 下次恢復的時候還可以接著運行, 上下文 (比如局部變量) 都還在.
這就是最大的區(qū)別.
二、協(xié)程的好處
考慮多任務協(xié)作的場景. 如果是線程的并發(fā), 那么大家需要搶 CPU 用, 還需要條件變量/信號量或者上鎖等技術, 來確保正確的線程正在工作.
如果在協(xié)程中, 大家就可以主動暫停自己, 多個任務互相協(xié)作. 這樣可能就比大家一起搶 CPU 更高效一點, 因為你能夠控制哪個協(xié)程用上 CPU.
一個例子:
生產(chǎn)者/消費者模型: 生產(chǎn)者生產(chǎn)完畢后, 暫停自己, 把控制流還給消費者. 消費者消費完畢后, resume 生產(chǎn)者, 生產(chǎn)者繼續(xù)生產(chǎn). 這樣循環(huán)往復.
異步調用: 比如你要請求網(wǎng)絡上的一個資源.
- 發(fā)請求給協(xié)程
- 協(xié)程收到請求以后, 發(fā)出請求. 協(xié)程暫停自己, 把控制權還回去.
- 你繼續(xù)做些別的事情. 比如發(fā)出下一個請求. 或者做一些計算.
- 恢復這個協(xié)程, 拿到資源 (可能還要再等一等)
理想狀態(tài)下, 4 可以直接用上資源, 這樣就完全不浪費時間.
如果是同步的話:
- 發(fā)請求給函數(shù).
- 函數(shù)收到請求以后, 等資源.
- 等了很久, 資源到了, 把控制權還回去.
明顯需要多等待一會兒. 如果需要發(fā)送上百個請求, 那顯然是第一種異步調用快一點. (等待的過程中可以發(fā)送新的請求)
如果沒有協(xié)程的話, 解決方案之一是使用多線程. 像這樣:
- 發(fā)請求給函數(shù).
- 函數(shù)在另外的線程等, 不阻塞你的線程.
- 你繼續(xù)做些別的事情. 比如發(fā)出下一個請求. 或者做一些計算.
- 等到終于等到了, 他再想一些辦法通知你.
然后通知的辦法就有 promise 和回調這些辦法.
三、協(xié)程得用法
我們照著 C++20 標準來看看怎么用協(xié)程. 用 g++, 版本 10.2 進行測試.
目前 C++20 標準只加入了協(xié)程的基本功能, 還沒有直接能上手用的類. GCC 說會盡量與 clang 和 MSVC 保持協(xié)程的 ABI 兼容, 同時和 libc++ 等保持庫的兼容. 所以本文可能也適用于它們.
協(xié)程和主程序之間通過 promise 進行通信. promise 可以理解成一個管道, 協(xié)程和其調用方都能看得到.
以前的 std::async 和 std::future 也是基于一種特殊的 promise 進行通信的, 就是 std::promise. 如果要使用協(xié)程, 則需要自己實現(xiàn)一個全新的 promise 類, 原理上是類似的.
四、協(xié)程三個關鍵字
這次引入了三個新的關鍵字 co_await, co_yield, co_return . 從效果上看: co_await 是用來暫停和恢復協(xié)程的, 并且真正用來求值.
co_yield 是用來暫停協(xié)程并且往綁定的 promise 里面 yield 一個值.
co_return 是往綁定的 promise 里面放入一個值.
這里我們先談談 co_yield 和 co_return. 談完這倆再談談 co_await 就比較簡單.
五、協(xié)程工作原理
所以最重要的兩個問題就是
- 協(xié)程如何實現(xiàn)信息傳遞 (使用自己實現(xiàn)的
promise) - 如何恢復一個已經(jīng)暫停了的協(xié)程 (使用
std::coroutine_handle)
上面說了, 一個協(xié)程會有一個與之相伴的 promise , 用作信息傳遞. 一個協(xié)程, 效果等同于
{
promise-type promise(promise-constructor-arguments);
try {
co_await promise.initial_suspend(); // 創(chuàng)建之后 第一次暫停
function-body // 函數(shù)體
} catch ( ... ) {
if (!initial-await-resume-called)
throw;
promise.unhandled_exception();
}
final-suspend:
co_await promise.final_suspend(); // 最后一次暫停
}
細節(jié), 包括 promise 初始化的參數(shù), 異常的處理等等, 我們留到之后的文章再處理. 所以我們簡化成
{
promise-type promise;
co_await promise.initial_suspend();
function-body // 函數(shù)體
final-suspend:
co_await promise.final_suspend();
}
對于暫停, co_await 那個地方就可以暫停并且交出控制權. 下篇文章我們會詳細介紹 co_await.
對于喚醒, 則需要拿到一個 std::coroutine_handle, 對它調用 resume() .
1、co_yield
co_yield 123 做的事情實際上相當于調用了 co_await promise.yield_value(123) . 這個 promise 里面存放了 123 以后, 會告訴 co_await 自己要暫停. 于是 co_await 就在這里停下來, 把控制流還回去.
來看一個標準中的實現(xiàn)范例.
#include <iostream>
#include <coroutine>
struct generator
{
struct promise_type;
using handle = std::coroutine_handle<promise_type>;
struct promise_type
{
int current_value;
static auto get_return_object_on_allocation_failure() { return generator{nullptr}; }
auto get_return_object() { return generator{handle::from_promise(*this)}; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() { return std::suspend_always{}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
auto yield_value(int value)
{
current_value = value;
return std::suspend_always{}; // 這是一個 awaiter 結構, 見第二篇文章
}
};
bool move_next() { return coro ? (coro.resume(), !coro.done()) : false; }
int current_value() { return coro.promise().current_value; }
generator(generator const &) = delete;
generator(generator &&rhs) : coro(rhs.coro) { rhs.coro = nullptr; }
~generator() { if (coro) coro.destroy(); }
private:
generator(handle h) : coro(h) {}
handle coro;
};
generator f()
{
co_yield 1;
co_yield 2;
}
int main()
{
auto g = f(); // 停在 initial_suspend 那里
while (g.move_next()) // 每次調用就停在下一個 co_await 那里
std::cout << g.current_value() << std::endl;
}
generator 是一個包裝類, 持有一個 std::coroutine_handle. 同時它規(guī)定了 coroutine_handle 本協(xié)程的 promise 是什么樣的. (通過 generator::promise_type告知)
coroutine_handle是協(xié)程的流程管理者, 由它來管理這個 promise. 而 generator 則是 coroutine_handle 的管理者.
f() 是一個協(xié)程. 可以展開成這樣的偽代碼
{
generator g(handle coro); // 建立句柄和包裝類
co_await promise.initial_suspend(); // 創(chuàng)建之后停在這里, 等待被恢復
co_await promise.yield_value(1); // 第一次恢復后就會停在這里
co_await promise.yield_value(2); // 第二次恢復后就會停在這里
final-suspend:
co_await promise.final_suspend(); // 第三次恢復后就會停在這里
}
按照這里的寫法, 每一次 promise.yield_value() 之后都會返回一個結構體給 co_await, 告訴 co_await 自己在這里暫停.
然后在主函數(shù)處調用 g.move_next() , 進而恢復了協(xié)程之后, 協(xié)程就會從剛剛暫停的 co_await 那一行恢復運行.
對了, 過了最后的 final_suspend() 以后, 這個協(xié)程就會析構掉. 再次恢復協(xié)程就會導致 segmentation fault.
g++10 已經(jīng)提供了協(xié)程的支持, 只需要加上 -std=c++20 -fcoroutines -fno-exceptions 即可. 上面這段代碼可以在這里編譯:
2、co_return
co_return 相當于調用了 promise.return_value() 或者 promise.return_void() 然后跳到 final-suspend 標簽那里. 也就是說這個這個協(xié)程結束了, 再也無法被恢復了.
而對比 co_yield 調用的是 co_await promise.yield_value(). 他們的區(qū)別就是 co_yeild 完了協(xié)程繼續(xù)等著下一次被恢復 , co_return 而 co_return完了協(xié)程就結束了. (為了讓協(xié)程也能像普通函數(shù)一樣返回)
我們來看一段代碼.
#include <iostream>
#include <future>
#include <coroutine>
using namespace std;
struct lazy
{
struct promise_type;
using handle = std::coroutine_handle<promise_type>;
struct promise_type
{
int _return_value;
static auto get_return_object_on_allocation_failure() { return lazy{nullptr}; }
auto get_return_object() { return lazy{handle::from_promise(*this)}; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() { return std::suspend_always{}; }
void unhandled_exception() { std::terminate(); }
void return_value(int value) { _return_value = value; }
};
bool calculate()
{
if (calculated)
return true;
if (!coro)
return false;
coro.resume();
if (coro.done())
calculated = true;
return calculated;
}
int get() { return coro.promise()._return_value; }
lazy(lazy const &) = delete;
lazy(lazy &&rhs) : coro(rhs.coro) { rhs.coro = nullptr; }
~lazy() { if (coro) coro.destroy(); }
private:
lazy(handle h) : coro(h) {}
handle coro;
bool calculated{false};
};
lazy f(int n = 0)
{
co_return n + 1;
}
int main()
{
auto g = f();
g.calculate(); // 這時才從 initial_suspend 之中恢復, 所以就叫 lazy 了
cout << g.get();
}
由于這個協(xié)程只能被恢復一次, 所以我稍稍修改了一下 lazy 的實現(xiàn). 可以參考這里:
下一篇C++20 新特性 協(xié)程 Coroutines(2)
到此這篇關于C++20 特性 協(xié)程 Coroutines的文章就介紹到這了,更多相關C++20 協(xié)程 Coroutines內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
C++實現(xiàn)雙目立體匹配Census算法的示例代碼
這篇文章主要為大家詳細介紹了如何利用C++實現(xiàn)雙目立體匹配Census算法,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下2022-08-08
C/C++雜記 虛函數(shù)的實現(xiàn)的基本原理(圖文)
這篇文章主要介紹了C/C++雜記 虛函數(shù)的實現(xiàn)的基本原理(圖文),需要的朋友可以參考下2016-06-06
詳解C++中對構造函數(shù)和賦值運算符的復制和移動操作
這篇文章主要介紹了C++中對構造函數(shù)和賦值運算符的復制和移動,是C++入門學習中的基礎知識,需要的朋友可以參考下2016-01-01

