C++20中的協(xié)程(Coroutine)的實(shí)現(xiàn)
C++20中的協(xié)程(Coroutine)
從2017年開始, 協(xié)程(Coroutine)的概念就開始被建議加入C++20的標(biāo)準(zhǔn)中了,并已經(jīng)開始有人對(duì)C++20協(xié)程的提案進(jìn)行了介紹。1事實(shí)上,協(xié)程的概念在很早就出現(xiàn)了,甚至其他語言(JS,Python,C#等)早就已經(jīng)支持了協(xié)程。
可見,協(xié)程并不是C++所特有的概念。
那么,什么是協(xié)程?
簡(jiǎn)單來說,協(xié)程就是一種特殊的函數(shù),它可以在函數(shù)執(zhí)行到某個(gè)地方的時(shí)候暫停執(zhí)行,返回給調(diào)用者或恢復(fù)者(可以有一個(gè)返回值),并允許隨后從暫停的地方恢復(fù)繼續(xù)執(zhí)行。注意,這個(gè)暫停執(zhí)行不是指將函數(shù)所在的線程暫停執(zhí)行,而是單純的暫停執(zhí)行函數(shù)本身。
那么,這種特殊函數(shù)有什么用呢?最常見的用途,就是將“異步”風(fēng)格的編程“同步”化。
比如,我們有一個(gè)請(qǐng)求webapi的庫,然后在某個(gè)應(yīng)用中我們需要發(fā)送一個(gè)http請(qǐng)求,然后等待web服務(wù)器反饋消息。恰巧的是,我們需要按順序請(qǐng)求多次,比如,只有請(qǐng)求A返回了,我們才能發(fā)送請(qǐng)求B,因?yàn)檎?qǐng)求B中包含請(qǐng)求A返回的結(jié)果。然后等請(qǐng)求B返回了,我們才能發(fā)送請(qǐng)求C等等。
我們不能阻塞主線程,那么此時(shí)我們應(yīng)該怎么辦?
最常見的思路就是開一個(gè)新線程,然后使用“回調(diào)函數(shù)”,例如:
// 示意代碼 void requestA(int req, std::function<void(int)> cb) { // 我們的webapi是異步調(diào)用, 我們開啟一個(gè)線程請(qǐng)求并等待調(diào)用完畢 std::thread t([req, cb]() { auto response = webapi.request(req); // 假定response有個(gè)等待返回值的接口waitForFinish,他會(huì)阻塞當(dāng)前線程,直到拿到返回值 int rt = response.waitForFinish(); // 返回了, 那么我們調(diào)用回調(diào)函數(shù) cb(rt); }); t.detach(); }
假定我們還有相同結(jié)構(gòu)的requestB,requestC以及其它, 那么我們會(huì)怎么用呢? 有了lamda表達(dá)式,通過回調(diào)函數(shù)進(jìn)行鏈?zhǔn)秸{(diào)用可以很簡(jiǎn)單的寫成如下形式:
int main() { requestA(1, [](int rt){ requestB(rt, [](int rt2){ requestC(rt2, [](int rt3){ // 根據(jù)需要可能會(huì)繼續(xù)嵌套下去 }); }); }); // 甚至可能需要再來一遍, 因?yàn)槲覀冞€需要使用另一個(gè)參數(shù)請(qǐng)求 requestA(2, [](int rt){ requestB(rt, [](int rt2){ requestC(rt2, [](int rt3){ // 根據(jù)需要可能會(huì)繼續(xù)嵌套下去 }); }); }); }
這還是好的,如果你使用Qt的信號(hào)槽來實(shí)現(xiàn),并同時(shí)可能有多個(gè)請(qǐng)求,你可能還會(huì)遇到另一個(gè)問題:“我怎么知道這個(gè)返回值是我發(fā)送的哪個(gè)請(qǐng)求產(chǎn)生的?”如果webapi庫沒有提供請(qǐng)求與反饋之間互相對(duì)應(yīng)的相關(guān)支持,你可能會(huì)更加的郁悶。
那么, 使用協(xié)程又會(huì)有哪些不一樣呢?
想象一下, 同樣的requestA,requestB,requestC,(當(dāng)然已經(jīng)修改為了協(xié)程的寫法) 你可以這么用
task<void> request() { int rt = co_await requestA(1); // 處理一些中間結(jié)果 rt = co_await requestB(rt); // 處理一些中間結(jié)果 rt = co_await requestC(rt); // 對(duì)最終結(jié)果做一些事情 }
這三個(gè)異步函數(shù)會(huì)在同一個(gè)線程中按照調(diào)用順序依次完成調(diào)用。
沒錯(cuò), 不再需要回調(diào)函數(shù), 你可以完全順序的, 仿佛異步調(diào)用不存在的使用同步調(diào)用的寫法。正是因?yàn)閰f(xié)程,我們就可以使用一個(gè)更加“同步”化的方式,實(shí)現(xiàn)異步調(diào)用了。
只要一個(gè)關(guān)鍵字co_await就能享用。隔壁的JavaScript早就用上了(ES6版本),現(xiàn)在,終于,C++也可以使用了!
那么這么好用的協(xié)程,是不是只要C++20一推出,我們加上一個(gè)關(guān)鍵字就能直接把異步調(diào)用轉(zhuǎn)化為同步調(diào)用呢?
很遺憾,并不能。
C++20的協(xié)程只是給了我們一個(gè)“使用同步風(fēng)格進(jìn)行異步調(diào)用”的框架,具體的實(shí)現(xiàn)還是需要我們自己去做。
如果你對(duì)JavaScript中的協(xié)程有所了解的話,就會(huì)明白,在ES6中,一個(gè)函數(shù)可以通過await等待返回的前提,是這個(gè)函數(shù)被聲明為async,而這是ES6提供的一個(gè)“語法糖”,也就是說,這個(gè)關(guān)鍵字只起到“提示”的作用,真正的實(shí)現(xiàn)是需要Promise的。
C++20中也是這樣,協(xié)程是特殊函數(shù),但是在C++20中,這個(gè)特殊函數(shù)不是由普通函數(shù)添加一個(gè)關(guān)鍵字組成的,我們需要為實(shí)現(xiàn)這個(gè)特殊函數(shù)做一些額外的工作。
目前,C++20應(yīng)該不會(huì)提供自動(dòng)化的包裝功能,或者簡(jiǎn)化包裝的庫,也就是說,想要讓某個(gè)函數(shù)成為協(xié)程函數(shù),我們需要人工的做一些額外的工作,一些輔助的自動(dòng)化的工具應(yīng)該會(huì)在C++23標(biāo)準(zhǔn)中提供,讓協(xié)程真正的可以被廣大開發(fā)人員使用。
雖然輔助工具再C++23才會(huì)提供,但是最基礎(chǔ)的已經(jīng)在C++20中存在了。
在我們繼續(xù)講解之前,先明確一些概念。
co_return,co_yield,co_await是為了使用協(xié)程而新增加的三個(gè)關(guān)鍵字,這些關(guān)鍵字在非協(xié)程函數(shù)中是無法使用的。這也就意味著,在main函數(shù)中直接調(diào)用co_await xxxx(); 是不行的。
這似乎有點(diǎn)違反我們的常識(shí)。協(xié)程的關(guān)鍵字只能在協(xié)程函數(shù)中使用有點(diǎn)遞歸的意思,這難道意味著普通的函數(shù)中沒法使用協(xié)程函數(shù)了?這其實(shí)是我們一開始聽說協(xié)程的描述時(shí)會(huì)產(chǎn)生的一種誤解。
為了消除這種誤解,我們先了解一下到底什么是協(xié)程函數(shù),以及它到底特殊在哪里。
協(xié)程函數(shù)和Awaitable類
接下來我們先從如何定義協(xié)程函數(shù)開始:
簡(jiǎn)單來說,就是如果一個(gè)函數(shù)的返回值是一個(gè)符合Promise規(guī)范的類,并且在這個(gè)函數(shù)中使用了co_return,co_yield,co_await中的一個(gè)或多個(gè),那么這個(gè)函數(shù)就是一個(gè)協(xié)程函數(shù)。
那么Promise規(guī)范又是啥?Promise在英文中是許諾的意思。簡(jiǎn)單來說,Promise規(guī)范就是:如果在類A中定義一個(gè)叫做promise_type的結(jié)構(gòu)體,并且其中包含特定名字的函數(shù),那么這個(gè)類A就符合Promise規(guī)范,它就是一個(gè)符合Promise規(guī)范的類,它也就是一個(gè)Promise。
比如以下例子:
struct task{ struct promise_type { auto get_return_object() { return task{}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() { return {}; } void return_void() {} void unhandled_exception() {} }; }
由于類task中定義了promise_type,同時(shí)其中包含了符合規(guī)范的5個(gè)函數(shù),它就是一個(gè)Promise。
然后根據(jù)協(xié)程規(guī)范,返回這個(gè)類的函數(shù)就是協(xié)程函數(shù),于是如果我們有以下定義:
task getTask() { // 實(shí)現(xiàn)中不需要返回task,也不能寫return co_return; }
getTask()就是一個(gè)協(xié)程函數(shù)了。當(dāng)然,如果協(xié)程函數(shù)中不使用co_wait或者co_yield其實(shí)就沒有什么意義。
然而,我們雖然有了協(xié)程函數(shù),但是我們依舊無法使用co_await,為什么呢?因?yàn)閏o_await關(guān)鍵字實(shí)際上是一個(gè)運(yùn)算符,其后面只能跟隨一個(gè)“實(shí)現(xiàn)了三個(gè)特定函數(shù)的類”。這三個(gè)特定函數(shù)如下所述:2
struct suspend_always { constexpr bool await_ready() const noexcept { return false; } constexpr void await_suspend(std::coroutine_handle<> h) const noexcept {} constexpr void await_resume() const noexcept {} };
注意,我們實(shí)現(xiàn)的時(shí)候只需要有包含這三個(gè)名字的函數(shù)就行了,并不需要繼承。
如果我們使用co_await suspend_always(); 會(huì)發(fā)生什么呢?
- suspend_always會(huì)被構(gòu)造,調(diào)用其構(gòu)造函數(shù)(一般情況下我們就可以通過構(gòu)造函數(shù)模仿一個(gè)普通的函數(shù)調(diào)用了)。
- 通過await_ready()判斷是否需要等待,如果返回true,就表示不需要等待,如果返回false,就表示需要等待。
- 如果不需要等待,則立刻執(zhí)行await_resume,否則先執(zhí)行await_suspend,然后進(jìn)入等待,調(diào)用co_await awaitable(); 的函數(shù)會(huì)在這里暫停運(yùn)行,但是不會(huì)影響所在線程的執(zhí)行。
- 我們可以在await_suspend函數(shù)中通過傳統(tǒng)的回調(diào)函數(shù)法執(zhí)行一些異步操作,然后在回調(diào)函數(shù)中調(diào)用std::coroutine_handle<>的resume函數(shù)主動(dòng)恢復(fù)。
- await_resume會(huì)在恢復(fù)執(zhí)行后立刻執(zhí)行,注意:co_wait的返回值就是該函數(shù)的返回值,而await_resume函數(shù)允許擁有任意的返回值類型,模板類型也是允許的。
也就是說可以使用以下的模板類讓co_wait的返回值更加的自由:3
template <class T> struct someAsyncOpt { bool await_ready() void await_suspend(std::coroutine_handle<>); T await_resume(); };
最后,我們也應(yīng)該了解,同一個(gè)線程在一個(gè)時(shí)間點(diǎn)最多只能跑一個(gè)協(xié)程;在同一個(gè)線程中,協(xié)程的運(yùn)行是穿行的,沒有數(shù)據(jù)爭(zhēng)用(data race),也不需要鎖。
至此,我們完成了協(xié)程的基本介紹。
那么,到底要如何使用協(xié)程呢?
了解了協(xié)程后我們就可以發(fā)現(xiàn)了以下事實(shí):
一個(gè)線程只能有一個(gè)協(xié)程
- 協(xié)程函數(shù)需要返回值是Promise
- 協(xié)程的所有關(guān)鍵字必須在協(xié)程函數(shù)中使用
- 在協(xié)程函數(shù)中可以按照同步的方式去調(diào)用異步函數(shù),只需要將異步函數(shù)包裝在Awaitable類中,使用co_wait關(guān)鍵字調(diào)用即可。
知道了以上事實(shí),我們就可以按照以下方式使用協(xié)程了:
- 在一個(gè)線程中同一個(gè)時(shí)間只調(diào)用一個(gè)協(xié)程函數(shù),即只有一個(gè)協(xié)程函數(shù)執(zhí)行完畢了,再去調(diào)用另一個(gè)協(xié)程函數(shù)。
- 使用Awatiable類包裝所有的異步函數(shù),一個(gè)異步函數(shù)處理一請(qǐng)求中的一部分工作(比如執(zhí)行一次SQL查詢,或者執(zhí)行一次http請(qǐng)求等)。
- 在對(duì)應(yīng)的協(xié)程函數(shù)中按照需要,通過增加co_wait關(guān)鍵字同步的調(diào)用這些異步函數(shù)。注意一個(gè)異步函數(shù)(包裝好的Awaiable類)可以在多個(gè)協(xié)程函數(shù)中調(diào)用,協(xié)程函數(shù)可能在多個(gè)線程中被調(diào)用(雖然一個(gè)線程同一時(shí)間只調(diào)用一個(gè)協(xié)程函數(shù)),所以最好保證Awaiable類是線程安全的,避免出現(xiàn)需要加鎖的情況。
- 在線程中通過調(diào)用不同的協(xié)程函數(shù)響應(yīng)不同的請(qǐng)求。
寫在最后
協(xié)程事實(shí)上并沒有消滅回調(diào)函數(shù),它只是為我們提供了一種方案,讓我們可以“用同步調(diào)用的方式進(jìn)行異步調(diào)用”。
回調(diào)函數(shù)還是存在的,只是被實(shí)現(xiàn)所隱藏起來了。
同時(shí),協(xié)程并不是只能用于“用同步調(diào)用的方式進(jìn)行異步調(diào)用”,它的本意其實(shí)就是“協(xié)同工作”。
也就是我等待你完成某個(gè)操作再去執(zhí)行其它的操作,和多線程類似,但是避免了資源競(jìng)爭(zhēng),因?yàn)橹挥幸粋€(gè)線程。
所有擁有類似需求的情況都可以使用協(xié)程來做。
目前C++20中協(xié)程只是剛剛出現(xiàn),作為一個(gè)基礎(chǔ)設(shè)施存在,因?yàn)槿狈Ρ匾妮o助支持的庫,直接使用協(xié)程反而會(huì)增加開發(fā)的復(fù)雜度和困難度。我們可以等待C++23為我們帶來一個(gè)更好用的協(xié)程,而現(xiàn)在我們需要的就是了解而已。
參考鏈接
https://lewissbaker.github.io/
C++20標(biāo)準(zhǔn)的草案n4849.pdf http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4849.pdf § 17.12.5
C++20標(biāo)準(zhǔn)的草案n4849.pdf http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4849.pdf § 7.6.2.3
到此這篇關(guān)于C++20中的協(xié)程(Coroutine)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)C++20 協(xié)程 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++中實(shí)現(xiàn)線程安全和延遲執(zhí)行詳解
這篇文章主要為大家詳細(xì)介紹了C++中實(shí)現(xiàn)線程安全和延遲執(zhí)行的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的小伙伴可以了解下2024-01-01C語言詳解結(jié)構(gòu)體的內(nèi)存對(duì)齊與大小計(jì)算
C 數(shù)組允許定義可存儲(chǔ)相同類型數(shù)據(jù)項(xiàng)的變量,結(jié)構(gòu)是 C 編程中另一種用戶自定義的可用的數(shù)據(jù)類型,它允許你存儲(chǔ)不同類型的數(shù)據(jù)項(xiàng),本篇讓我們來了解C 的結(jié)構(gòu)體內(nèi)存對(duì)齊與計(jì)算大小2022-04-04C語言實(shí)現(xiàn)學(xué)生選課系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)學(xué)生選課系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02char str[] 與 char *str的區(qū)別詳細(xì)解析
以下是對(duì)char str[]與char *str的區(qū)別進(jìn)行了詳細(xì)的介紹,需要的朋友可以過來參考下2013-09-09C++普通函數(shù)指針與成員函數(shù)指針實(shí)例解析
這篇文章主要介紹了C++普通函數(shù)指針與成員函數(shù)指針,很重要的知識(shí)點(diǎn),需要的朋友可以參考下2014-08-08C語言動(dòng)態(tài)分配二維字符串?dāng)?shù)組的方法
小編最近忙里偷閑,給大家整理一份教程關(guān)于C語言動(dòng)態(tài)分配二維字符串?dāng)?shù)組的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-10-10