如何編寫(xiě) C 20 協(xié)程(Coroutines)
花了一兩周的時(shí)間后,我想寫(xiě)寫(xiě) C 20 協(xié)程的基本用法,因?yàn)?C 的協(xié)程讓我感到很奇怪,寫(xiě)一個(gè)協(xié)程程序十分費(fèi)勁。讓我們拋去復(fù)雜的東西,來(lái)看看寫(xiě)一個(gè) C 協(xié)程需要哪些東西。
編譯器支持
由于 C 20 還沒(méi)被所有編譯器完全支持,首先需要確保你的編譯器實(shí)現(xiàn)了 Coroutines,可以通過(guò)下面的網(wǎng)站查看編譯器支持情況:https://en.cppreference.com/w/cpp/compiler_support#cpp20值得一提,我使用的 MacOS 自帶的 Apple Clang 對(duì) C 20 支持很弱,我選擇通過(guò) Homebrew 安裝最新版的 GNU GCC (10 以上版本)來(lái)編譯。我使用的 GNU GCC 10.2 版本編譯指令:g -fcoroutines -std=c 20
Clang 支持不夠好,不推薦使用。Clang 可以使用如下命令編譯:clang -std=c 20 -stdlib=libc -fcoroutines-ts
不推薦 Clang 還有一個(gè)理由:使用 Clang 需要 include 頭文件?
?而不是?
。此外,一些類(lèi)型被命名為?std::experimental:xxx
?而不是?std:xxx
。以下示例代碼只支持 GNU GCC 版本的編譯器。C 協(xié)程簡(jiǎn)介
在正式開(kāi)始之前,我們先要理解 C 20 中協(xié)程使用的一些術(shù)語(yǔ)。首先,什么是協(xié)程?協(xié)程就是一個(gè)可以掛起(suspend)和恢復(fù)(resume)的函數(shù)(但無(wú)論如何不能是 main 函數(shù))。你可以暫停協(xié)程的執(zhí)行,去做其他事情,然后在適當(dāng)?shù)臅r(shí)候恢復(fù)到暫停的位置繼續(xù)執(zhí)行。協(xié)程讓我們使用同步方式寫(xiě)異步代碼。怎么掛起協(xié)程呢?C 提供了三個(gè)方法:co_await
,?co_yield
?和?co_return
。順便說(shuō)一句:coroutine 不是并行(parallelism),和 Go 語(yǔ)言的 goroutine 不一樣!與你之前接觸到的協(xié)程完全不同,一個(gè) C 協(xié)程一般長(zhǎng)這樣:這奇怪的協(xié)程代碼涉及了 C 協(xié)程很重要的三個(gè)概念:
promise_type
Awaitable
std::coroutine_handle<>
Promise
C 協(xié)程的返回類(lèi)型必須是?promise_type
,promise_type
?是一個(gè) interface,你可以用它來(lái)控制協(xié)程,在協(xié)程的生命周期中注入自定義行為:get_return_object
:控制協(xié)程的返回對(duì)象initial_suspend
:在協(xié)程開(kāi)始的時(shí)候掛起final_suspend
:在協(xié)程結(jié)束的時(shí)候掛起
?被包裹在下面的偽代碼中(來(lái)源:http://eel.is/c draft/dcl.fct.def.coroutine#5):可以看到,initial_suspend
?會(huì)在進(jìn)入?yún)f(xié)程(也就是函數(shù))之前執(zhí)行,final_suspend
?會(huì)在協(xié)程返回之前執(zhí)行。如果?
final_suspend
?真的掛起了協(xié)程,那么作為協(xié)程的調(diào)用者,你需要手動(dòng)的調(diào)用 destroy 來(lái)釋放協(xié)程;如果?final_suspend
?沒(méi)有掛起協(xié)程,那么協(xié)程將自動(dòng)銷(xiāo)毀。先記住這句話,在后面還會(huì)提到。除此之外,Promise 還有一些其它責(zé)任:return_void()
/return_value()
/yield_value()
?方法: 用來(lái)控制?co_return
?和?co_yield
的行為;unhandled_exception()
?處理異常- 創(chuàng)建和銷(xiāo)毀協(xié)程的?
stackframe
- 處理?
stackframe
?創(chuàng)建可能發(fā)生的異常
stackframe :函數(shù)運(yùn)行時(shí)占用的內(nèi)存空間,是棧上的數(shù)據(jù)集合,它包括:
- Local variables
- Saved copies of registers modified by subprograms that could need restoration
- Argument parameters
- Return address
Awaitable
第二個(gè)概念是?Awaitable
,Awaitable
?負(fù)責(zé)管理協(xié)程掛起時(shí)的行為。一個(gè) Awaitable 對(duì)象可以成為?co_await
?調(diào)用的對(duì)象。Awaitable 擁有以下方法:await_ready()
:是否要掛起,如果返回 true,那么?co_await
?就不會(huì)掛起函數(shù);await_resume()
:co_await
?的返回值,通常返回空;?await_suspend()
:協(xié)程掛起時(shí)的行為;
可以在?有時(shí)候我們的協(xié)程并不需要自定義復(fù)雜的行為,C 提供了兩個(gè)默認(rèn)的?await_suspend
?中實(shí)現(xiàn)?await_ready
?的效果,例如直接不掛起當(dāng)前的協(xié)程,但在調(diào)用?await_suspend
?之前,編譯器必須將所有狀態(tài)捆綁到協(xié)程的?stackframe
?中,這會(huì)更耗時(shí)。
Awaitable
:suspend_always::await_ready()
?總是返回 false,而?suspend_always::await_ready()
?總是返回 true。其他的方法都是空的,沒(méi)有任何作用。如果沒(méi)有其它多余的行為,我們可以在函數(shù)中直接調(diào)用?co_await std::suspend_always{}
?來(lái)掛起一個(gè)函數(shù)。Coroutine Handle
co_await
?掛起函數(shù),并創(chuàng)建了一個(gè)可調(diào)用對(duì)象,這個(gè)對(duì)象可以用來(lái)恢復(fù)Hanns乎的執(zhí)行。這個(gè)可調(diào)用對(duì)象的類(lèi)型就是?std::coroutine_handle<>
,最常用的兩個(gè)方法是:handle.resume()
:恢復(fù)協(xié)程的執(zhí)行;handle.destroy()
:銷(xiāo)毀協(xié)程;
Coroutine Handle
?很像指針,我們可以復(fù)制它,但析構(gòu)函數(shù)不會(huì)釋放相關(guān)狀態(tài)的內(nèi)存。為了避免內(nèi)存泄漏,一般要調(diào)用?handle.destroy()
?來(lái)釋放(盡管在某些情況下,協(xié)程會(huì)在完成后自行銷(xiāo)毀——前文有提到)。同樣像指針一樣,一旦銷(xiāo)毀了一個(gè)?Coroutine Handle
?,指向同一個(gè)協(xié)程的另一個(gè)?Coroutine Handle
?將指向垃圾,并在調(diào)用時(shí)表現(xiàn)出未定義行為。學(xué)習(xí)更復(fù)雜的用法之前,我們先看下示例。示例
這個(gè)簡(jiǎn)短的示例展示了 C 實(shí)現(xiàn)協(xié)程 "Hello world" 程序。我們執(zhí)行完 "Hello " 后掛起函數(shù),又在執(zhí)行?handle.resume()
?后恢復(fù)函數(shù)的運(yùn)行。非常簡(jiǎn)單,不再過(guò)多解釋。co_yield
C 協(xié)程與一個(gè) Promise 交互之所以如此笨拙,有一個(gè)特殊原因就是為了?co_yield
。如果 promise 是當(dāng)前協(xié)程的 Promise 對(duì)象,那么執(zhí)行:co_yield <expression>;
相當(dāng)于執(zhí)行了:co_await promise.yield_value(<expression>);
所以,需要在 promise_type 中添加一個(gè)?yield_value
?方法。上面的例子可以改為:可以用?co_yield
?實(shí)現(xiàn) Python 中的生成器,參考:https://lewissbaker.github.io/2018/09/05/understanding-the-promise-typeco_return
執(zhí)行?co_return
?語(yǔ)句時(shí):co_return <expression>;
相當(dāng)于執(zhí)行了:co_return promise.return_value(<expression>); goto end;
下面再給出示例加上?co_return
?的版本:復(fù)雜一些
到此,?Awaitable
?和?Coroutine Handle
?好像還沒(méi)有發(fā)揮什么作用,我寫(xiě)的示例程序都非常簡(jiǎn)單。如果我們想在協(xié)程掛起的時(shí)候,做更多的動(dòng)作,一般將?Coroutine Handle
?傳到 Awaitable 的?await_suspend()
?中,用一個(gè)官網(wǎng)的例子展示一下:小結(jié)
本文簡(jiǎn)單介紹了 C 協(xié)程,希望下次你寫(xiě) C 協(xié)程的時(shí)候,首先想到這三個(gè)東西:我本人也不是編程語(yǔ)言專(zhuān)家,對(duì)于 C 協(xié)程總覺(jué)得有些繁瑣、怪異,或許是我并不清楚 C 在原有情況下支持協(xié)程的困難,但我依然覺(jué)得 C 團(tuán)隊(duì)可以做得更好。我還需要花時(shí)間弄明白到底該如何在項(xiàng)目中使用這臃腫的協(xié)程。不過(guò),可以預(yù)見(jiàn)到的是,我們會(huì)在越來(lái)越多的 C 項(xiàng)目中看到協(xié)程的身影。比如 facebook folly 就已經(jīng)實(shí)現(xiàn)了一個(gè)實(shí)驗(yàn)階段的協(xié)程框架: https://github.com/facebook/folly/tree/master/folly/experimental/coro也許等我再研究一段時(shí)間,會(huì)寫(xiě)一篇到底該如何使用 C 協(xié)程。Reference
- C Coroutine definitions:?http://eel.is/c draft/dcl.fct.def.coroutine#5
- C draft expr.await:?http://eel.is/c draft/expr.await
- C Coroutines: Understanding the promise type:?https://lewissbaker.github.io/2018/09/05/understanding-the-promise-type
- 官網(wǎng)的例子:https://en.cppreference.com/w/cpp/language/coroutines
- My tutorial and take on C 20 coroutines:https://www.scs.stanford.edu/~dm/blog/c -coroutines.html#coroutine-handles