您好,登錄后才能下訂單哦!
本篇內容介紹了“如何理解C++20特性協程”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
一、協程簡單介紹
二、協程的好處
三、協程得用法
四、協程三個關鍵字
五、協程工作原理
1、co_yield
2、co_return
我們先來介紹一下什么是協程.
協程和普通的函數 其實差不多. 不過這個 "函數" 能夠暫停自己, 也能夠被別人恢復.
普通的函數調用, 函數運行完返回一個值, 結束.
協程可以運行到一半, 返回一個值, 并且保留上下文. 下次恢復的時候還可以接著運行, 上下文 (比如局部變量) 都還在.
這就是最大的區別.
考慮多任務協作的場景. 如果是線程的并發, 那么大家需要搶 CPU
用, 還需要條件變量/信號量或者上鎖等技術, 來確保正確的線程正在工作.
如果在協程中, 大家就可以主動暫停自己, 多個任務互相協作. 這樣可能就比大家一起搶 CPU
更高效一點, 因為你能夠控制哪個協程用上 CPU
.
一個例子:
生產者/消費者模型: 生產者生產完畢后, 暫停自己, 把控制流還給消費者. 消費者消費完畢后, resume
生產者, 生產者繼續生產. 這樣循環往復.
異步調用: 比如你要請求網絡上的一個資源.
發請求給協程
協程收到請求以后, 發出請求. 協程暫停自己, 把控制權還回去.
你繼續做些別的事情. 比如發出下一個請求. 或者做一些計算.
恢復這個協程, 拿到資源 (可能還要再等一等)
理想狀態下, 4 可以直接用上資源, 這樣就完全不浪費時間.
如果是同步的話:
發請求給函數.
函數收到請求以后, 等資源.
等了很久, 資源到了, 把控制權還回去.
明顯需要多等待一會兒. 如果需要發送上百個請求, 那顯然是第一種異步調用快一點. (等待的過程中可以發送新的請求)
如果沒有協程的話, 解決方案之一是使用多線程. 像這樣:
發請求給函數.
函數在另外的線程等, 不阻塞你的線程.
你繼續做些別的事情. 比如發出下一個請求. 或者做一些計算.
等到終于等到了, 他再想一些辦法通知你.
然后通知的辦法就有 promise
和回調這些辦法.
我們照著 C++20 標準來看看怎么用協程. 用 g++, 版本 10.2 進行測試.
目前 C++20 標準只加入了協程的基本功能, 還沒有直接能上手用的類. GCC 說會盡量與 clang
和 MSVC
保持協程的 ABI 兼容, 同時和 libc++ 等保持庫的兼容. 所以本文可能也適用于它們.
協程和主程序之間通過 promise
進行通信. promise
可以理解成一個管道, 協程和其調用方都能看得到.
以前的 std::async
和 std::future
也是基于一種特殊的 promise
進行通信的, 就是 std::promise
. 如果要使用協程, 則需要自己實現一個全新的 promise
類, 原理上是類似的.
這次引入了三個新的關鍵字 co_await
, co_yield
, co_return
. 從效果上看: co_await 是用來暫停和恢復協程的, 并且真正用來求值.
co_yield 是用來暫停協程并且往綁定的 promise
里面 yield
一個值.
co_return 是往綁定的 promise
里面放入一個值.
這里我們先談談 co_yield
和 co_return.
談完這倆再談談 co_await
就比較簡單.
所以最重要的兩個問題就是
協程如何實現信息傳遞 (使用自己實現的 promise
)
如何恢復一個已經暫停了的協程 (使用 std::coroutine_handle
)
上面說了, 一個協程會有一個與之相伴的 promise
, 用作信息傳遞. 一個協程, 效果等同于
{ promise-type promise(promise-constructor-arguments); try { co_await promise.initial_suspend(); // 創建之后 第一次暫停 function-body // 函數體 } catch ( ... ) { if (!initial-await-resume-called) throw; promise.unhandled_exception(); } final-suspend: co_await promise.final_suspend(); // 最后一次暫停 }
細節, 包括 promise
初始化的參數, 異常的處理等等, 我們留到之后的文章再處理. 所以我們簡化成
{ promise-type promise; co_await promise.initial_suspend(); function-body // 函數體 final-suspend: co_await promise.final_suspend(); }
對于暫停, co_await
那個地方就可以暫停并且交出控制權. 下篇文章我們會詳細介紹 co_await
.
對于喚醒, 則需要拿到一個 std::coroutine_handle
, 對它調用 resume()
.
co_yield 123
做的事情實際上相當于調用了 co_await promise.yield_value(123)
. 這個 promise
里面存放了 123 以后, 會告訴 co_await 自己要暫停. 于是 co_await
就在這里停下來, 把控制流還回去.
來看一個標準中的實現范例.
#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
. 同時它規定了 coroutine_handle
本協程的 promise
是什么樣的. (通過 generator::promise_type
告知)
coroutine_handle
是協程的流程管理者, 由它來管理這個 promise. 而 generator 則是 coroutine_handle
的管理者.
f() 是一個協程. 可以展開成這樣的偽代碼
{ generator g(handle coro); // 建立句柄和包裝類 co_await promise.initial_suspend(); // 創建之后停在這里, 等待被恢復 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
自己在這里暫停.
然后在主函數處調用 g.move_next()
, 進而恢復了協程之后, 協程就會從剛剛暫停的 co_await
那一行恢復運行.
對了, 過了最后的 final_suspend()
以后, 這個協程就會析構掉. 再次恢復協程就會導致 segmentation fault
.
g++10 已經提供了協程的支持, 只需要加上 -std=c++20 -fcoroutines -fno-exceptions 即可. 上面這段代碼可以在這里編譯:
co_return
相當于調用了 promise.return_value()
或者 promise.return_void()
然后跳到 final-suspend
標簽那里. 也就是說這個這個協程結束了, 再也無法被恢復了.
而對比 co_yield
調用的是 co_await promise.yield_value().
他們的區別就是 co_yeild
完了協程繼續等著下一次被恢復 , co_return
而 co_return
完了協程就結束了. (為了讓協程也能像普通函數一樣返回)
我們來看一段代碼.
#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(); }
“如何理解C++20特性協程”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。