C++ 20 協程 Coroutine之剖析
我們來剖析一下協程的過程。通過這個剖析,希望達到梳理協程幾個重要概念的關系,把這些點串起來。所以在概念參考我們列出了相應的概念文字。
協程的創建
C++20協程在啟動前,開始會new 一個協程狀態(coroutine state?)。然后構造協程的承諾對象(promise?)。承諾對象(promise?)通過get_return_object()?構造協程的返回值result?。這個返回值在協程第一次掛起時,賦值給調用者。然后通過co_await promise.initial_suspend()?,決定協程初試完成后的行為。如果返回std::suspend_always?,初始化就掛起,如果返回std::suspend_never ,初始化后就繼續運行。(注意initial_suspend也可以返回其他協程體)
協程的co_await
cw_ret = co_await awaiter? 或者cw_ret = co_await fun()?,先計算表達式fun,fun返回結果,就是一個等待體awaiter?。系統先調用awaiter.await_ready()?接口,看等待體是否準備好了,沒準備好(return false?)就調用awaiter.await_suspend()。await_suspend?根據參數可以記錄調用其的協程的的句柄。await_suspend?的返回值為return true? ,或者 return void 就會掛起協程。
后面在外部如果恢復了協程的運行,awaiter.await_resume()?接口被調用。其返回結果,作為co_await的返回值。
協程的co_yield
co_yield cy_ret;?,相當于調用co_wait promise.yield_value(cy_ret)?,你可以在yield_value?中記錄參數cy_ret?后面使用,yield_value?的返回值如果是std::suspend_always?,協程掛起,如果返回std::suspend_never ,協程就繼續運行。
協程的co_return
co_yield cr_ret;?,調用promise.retun_value(cr_ret)?,如果沒有返回值相當于promise.retun_viod()?,你可以在retun_value?中記錄參數cr_ret?后面使用。然后調用co_await promise.final_suspend(void)?,如果返回值是std::suspend_always?,你需要自己手動青清理coroutine handle?,調用handle.destroy()。
這兒存在一個疑問,final_suspend?,并沒有真正掛起協程。看C++ 參考,里面說的也是calls promise.final_suspend() and co_awaits the result.?。按說如果返回應該要掛起。但用VS 2022測試是不會掛起的,再探 C++20 協程文章中說的是如果返回std::suspend_always?,需要你自己清理coroutine handle。存疑吧。
概念參考附錄:
這些概念在原文第一章都有,附錄在此僅供您方便參考。
協程狀態(coroutine state)
協程狀態(coroutine state)是協程啟動開始時,new空間存放協程狀態,協程狀態記錄協程函數的參數,協程的運行狀態,變量。掛起時的斷點。
注意,協程狀態 (coroutine state?)并不是就是協程函數的返回值RET。雖然我們設計的RET一般里面也有promise和coroutine handle?,大家一般也是通過RET去操作協程的恢復,獲取返回值。但coroutine state?理論上還應該包含協程運行參數,斷點等信息。而協程狀態 (coroutine state?)應該是協程句柄(coroutine handle)對應的一個數據,而由系統管理的。
承諾對象(promise)
承諾對象的表現形式必須是result::promise_type,result為協程函數的返回值。
承諾對象是一個實現若干接口,用于輔助協程,構造協程函數返回值;提交傳遞co_yield,co_return的返回值。明確協程啟動階段是否立即掛起;以及協程內部發生異常時的處理方式。其接口包括:
- auto get_return_object() :用于生成協程函數的返回對象。
- auto initial_suspend():用于明確初始化后,協程函數的執行行為,返回值為等待體(awaiter),用co_wait調用其返回值。返回值為std::suspend_always 表示協程啟動后立即掛起(不執行第一行協程函數的代碼),返回std::suspend_never 表示協程啟動后不立即掛起。(當然既然是返回等待體,你可以自己在這兒選擇進行什么等待操作)
- void return_value(T v):調用co_return v后會調用這個函數,可以保存co_return的結果
- auto yield_value(T v):調用co_yield后會調用這個函數,可以保存co_yield的結果,其返回其返回值為std::suspend_always表示協程會掛起,如果返回std::suspend_never表示不掛起。
- auto final_suspend() noexcept:在協程退出是調用的接口,返回std::suspend_never ,自動銷毀 coroutine state 對象。若 final_suspend 返回 std::suspend_always 則需要用戶自行調用 handle.destroy() 進行銷毀。但值得注意的是返回std::suspend_always并不會掛起協程。
前面我們提到在協程創建的時候,會new協程狀態(coroutine state?)。你可以通過可以在 promise_type? 中重載 operator new? 和 operator delete,使用自己的內存分配接口。(請參考再探 C++20 協程)
協程句柄(coroutine handle)
協程句柄(coroutine handle)是一個協程的標示,用于操作協程恢復,銷毀的句柄。
協程句柄的表現形式是std::coroutine_handle<promise_type>?,其模板參數為承諾對象(promise)類型。句柄有幾個重要函數:
- resume()函數可以恢復協程。
- done()函數可以判斷協程是否已經完成。返回false標示協程還沒有完成,還在掛起。
協程句柄和承諾對象之間是可以相互轉化的。
- std::coroutine_handle<promise_type>::from_promise :這是一個靜態函數,可以從承諾對象(promise)得到相應句柄。
- std::coroutine_handle<promise_type>::promise() 函數可以從協程句柄coroutine handle得到對應的承諾對象(promise)
等待體(awaiter)
co_wait 關鍵字會調用一個等待體對象(awaiter)。這個對象內部也有3個接口。根據接口co_wait 決定進行什么操作。
- bool await_ready():等待體是否準備好了,返回 false ,表示協程沒有準備好,立即調用await_suspend。返回true,表示已經準備好了。
- auto await_suspend(std::coroutine_handle<> handle)如果要掛起,調用的接口。其中handle參數就是調用等待體的協程,其返回值有3種可能
void 同返回true
bool 返回true 立即掛起,返回false 不掛起。
返回某個協程句柄(coroutine handle),立即恢復對應句柄的運行。
- auto await_resume() :協程掛起后恢復時,調用的接口。返回值作為co_wait 操作的返回值。
等待體(awaiter)值得用更加詳細的筆墨書寫一章,我們就放一下,先了解其有2個特化類型。
- std::suspend_never類,不掛起的的特化等待體類型。
- std::suspend_always類,掛起的特化等待體類型。
前面不少接口已經用了這2個特化的類,同時也可以明白其實協程內部不少地方其實也在使用co_wait 關鍵字。
本章總結
此章講解了協程的啟動,3個關鍵字的細節。您可以通過這些關鍵概念,融合協程狀態(coroutine state?),承諾對象(promise?),協程句柄(coroutine handle?),等待體(awaiter)。
參考文檔
初探 C++20 協程
再探 C++20 協程
Coroutines (C++20)
協程(coroutine)簡介
The Coroutine in C++ 20 協程之諾
C++ Coroutines: Understanding operator co_await