C 20 协程初探
【導讀】:C 20 終于引入了協程特性,給庫作者提供了一個實現協程的機制,讓用戶方便使用協程來編寫異步邏輯,降低了異步并發編程的難度。結合我最近協程的學習,在這里記錄一下相關內容。
以下是正文
使用場景
協程和普通函數相比,多了個中途隨時?掛起?,隨后?恢復?的過程,當用戶調用一個阻塞請求接口,從而讓出控制權,當響應時,恢復之前的控制流,從而大大提高線程復用率,這也注意了協程只是并發的,并不是真正意義上的并行,在 IO 密集型場景下,協程能夠很好的提高資源利用率,用少數的線程達到并發成百上萬個協程的效果。
而相對傳統的線程池 回調模式,每發起一個請求,為了避免阻塞當前線程,需要掛一個回調函數處理后續過程,而回調函數又可能產生競爭,導致得加鎖處理。而協程卻能夠以同步方式寫實現異步,后續過程直接掛起,當響應的時候恢復執行。
我參與的項目中,對象隨時都可能起個線程干活,或者常駐于對象生命周期里,統計下來整個項目居然開了幾百個線程,由于多線程編程難免導致競爭,從而需要鎖這種很低級的機制做同步,而一旦引入了鎖,就不可避免的擴散開來,大家看到這里加把鎖,那我也加把鎖,統計下來代碼里面居然也有幾百把鎖。真是維護的噩夢。
由于協程能夠隨時掛起,后續恢復,這就能實現一些延遲計算的特性,例如生成器。
扯遠了,本文主題是關于 C 20 的協程,在 C 20 還沒穩定之前,先來學習一下相關知識,讀完本文后你應該能利用這個機制實現一些想要的協程了。
概念模型
C 20 的協程設計為無棧協程,相對于有棧協程,省掉了上下文切換開銷[1],只能手動切換,效率更高,也不用管理復雜的寄存器狀態,移植性更好,但這同時也導致了不能被非協程函數嵌套調用。
同時引入了 3 個關鍵字:
1. co_yield: 掛起并返回值
2. co_await: 掛起
3. co_return: 結束協程
當一個函數出現了上面的關鍵字,則該函數是個協程。
Promise
當 caller 調用一個 callee 協程的時候,協程自身的狀態信息?[2](形參,局部變量,自帶數據,各個階段點執行點)會被保存在堆上的 Promise 對象中,這也是編譯器會在協程里面插入 Promise 相關代碼,以及一些執行點。由于 Promise 的大小可以在編譯期計算出來,從而避免了內存浪費。而 Promise 對象所有權可由coroutine_handle 句柄持有。
Future
而 Future 對象主要是與 Promise 對象交互的橋梁,既 caller 與 callee 之間的通信:
1. callee 掛起時,將值返回給 caller: yield 語義
2. callee 執行結束時,將值返回給 caller: return 語義
3. callee 恢復時,caller 將值帶給 callee
需要注意的是,這些概念和標準庫的 std::promise/std::future 不是同一個東西,后者用于做同步用,std::future會阻塞等待直到 std::promise 提供值,可以看做是條件變量的封裝,同樣地,和其他語言的 Promise/Future 概念也不一樣。
Awaitable
如果一個對象是 Awaitable 對象,那么可以用 co_await 操作符去觸發該對象的動作 ready/suspend/resume,從而轉移、恢復控制權,co_await 細節留到后面在介紹。
具體機制
了解了概念模型后,我們可以進一步探討背后的機制了。
Promise/Future 對象
當一個協程被調用時,會創建 Promise 對象,然后編譯器會在各個階段插入一些代碼[3]:
{??co_await?promise.initial_suspend();??try??{??????}??catch?(...)??{????promise.unhandled_exception();??}FinalSuspend:??co_await?promise.final_suspend();}可以看到一個協程函數,分為如下幾個步驟:
1. 從堆上 (operator new) 創建 Promise 對象,保存協程的狀態信息
2. initial_suspend 階段,用于在執行協程主體??代碼前做些事情
3. 階段,執行協程的主體代碼
4. unhandled_exception 階段,若拋異常,處理異常
5. final_suspend階段,協程結束收尾動作,在這階段的 coroutine_handle::done 方法為 true,caller 可以通過這個方法判斷協程是否結束,從而不再調用 resume 恢復協程。
而協程返回類型則是一個 Future 對象,這一步編譯器通過 Promise::get_return_object()?來創建 Future 對象。而 Future 對象一般持有 Promise 的句柄:coroutine_handle,這樣 caller 可以通過 Future 與 Promise 交互,從而恢復協程。
而 Promise 對象釋放的時間點有兩個,避免重復執行,否則會 double free:
1. final_suspend 階段 resume 后
2. 調用?coroutine_handle::destroy()?方法
比較好的做法是在 final_suspend 階段掛起,這時候就不可 resume 了,在 caller 通過調用 Future 持有的句柄 destroy()?方法釋放 Promise 對象。綜上,一個 Promise 對象需要實現如下方法:
1. initial_suspend: 返回一個 Awaitable 對象
總結
- 上一篇: C 多线程的互斥锁应用RAII机制
- 下一篇: 如何写一个简单的node.js C 扩