這篇文章主要介紹“C++20協(xié)程的使用方法”,在日常操作中,相信很多人在C++20協(xié)程的使用方法問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”C++20協(xié)程的使用方法”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
創(chuàng)新互聯(lián)公司專注于企業(yè)全網(wǎng)整合營銷推廣、網(wǎng)站重做改版、白云鄂網(wǎng)站定制設(shè)計、自適應(yīng)品牌網(wǎng)站建設(shè)、成都h5網(wǎng)站建設(shè)、成都做商城網(wǎng)站、集團公司官網(wǎng)建設(shè)、成都外貿(mào)網(wǎng)站建設(shè)、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計等建站業(yè)務(wù),價格優(yōu)惠性價比高,為白云鄂等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。
摘要:事件驅(qū)動(event driven)是一種常見的代碼模型,其通常會有一個主循環(huán)(mainloop)不斷的從隊列中接收事件,然后分發(fā)給相應(yīng)的函數(shù)/模塊處理。常見使用事件驅(qū)動模型的軟件包括圖形用戶界面(GUI),嵌入式設(shè)備軟件,網(wǎng)絡(luò)服務(wù)端等。
事件驅(qū)動(event driven)是一種常見的代碼模型,其通常會有一個主循環(huán)(mainloop)不斷的從隊列中接收事件,然后分發(fā)給相應(yīng)的函數(shù)/模塊處理。常見使用事件驅(qū)動模型的軟件包括圖形用戶界面(GUI),嵌入式設(shè)備軟件,網(wǎng)絡(luò)服務(wù)端等。
本文以一個高度簡化的嵌入式處理模塊做為事件驅(qū)動代碼的例子:假設(shè)該模塊需要處理用戶命令、外部消息、告警等各種事件,并在主循環(huán)中進行分發(fā),那么示例代碼如下:
#include <iostream> #include <vector> enum class EventType { COMMAND, MESSAGE, ALARM }; // 僅用于模擬接收的事件序列 std::vector<EventType> g_events{EventType::MESSAGE, EventType::COMMAND, EventType::MESSAGE}; void ProcessCmd() { std::cout << "Processing Command" << std::endl; } void ProcessMsg() { std::cout << "Processing Message" << std::endl; } void ProcessAlm() { std::cout << "Processing Alarm" << std::endl; } int main() { for (auto event : g_events) { switch (event) { case EventType::COMMAND: ProcessCmd(); break; case EventType::MESSAGE: ProcessMsg(); break; case EventType::ALARM: ProcessAlm(); break; } } return 0; }
這只是一個極簡的模型示例,真實的代碼要遠比它復(fù)雜得多,可能還會包含:從特定接口獲取事件,解析不同的事件類型,使用表驅(qū)動方法進行分發(fā)……不過這些和本文關(guān)系不大,可暫時先忽略。
用順序圖表示這個模型,大體上是這樣:
在實際項目中,常常碰到的一個問題是:有些事件的處理時間很長,比如某個命令可能需要批量的進行上千次硬件操作:
void ProcessCmd() { for (int i{0}; i < 1000; ++i) { // 操作硬件接口…… } }
這種事件處理函數(shù)會長時間的阻塞主循環(huán),導(dǎo)致其他事件一直排隊等待。如果所有事件對響應(yīng)速度都沒有要求,那也不會造成問題。但是實際場景中經(jīng)常會有些事件是需要及時響應(yīng)的,比如某些告警事件出現(xiàn)后,需要很快的執(zhí)行業(yè)務(wù)倒換,否則就會給用戶造成損失。這個時候,處理時間很長的事件就會產(chǎn)生問題。
有人會想到額外增加一個線程專用于處理高優(yōu)先級事件,實踐中這確實是個常用方法。然而在嵌入式系統(tǒng)中,事件處理函數(shù)會讀寫很多公共數(shù)據(jù)結(jié)構(gòu),還會操作硬件接口,如果并發(fā)調(diào)用,極容易導(dǎo)致各類數(shù)據(jù)競爭和硬件操作沖突,而且這些問題常常很難定位和解決。那在多線程的基礎(chǔ)上加鎖呢?——設(shè)計哪些鎖,加在哪些地方,也是非常燒腦而且容易出錯的工作,如果互斥等待過多,還會影響性能,甚至出現(xiàn)死鎖等麻煩的問題。
另一種解決方案是:把處理時間很長的任務(wù)切割成很多個小任務(wù),并重新加入到事件隊列中。這樣就不會長時間的阻塞主循環(huán)。這個方案避免了并發(fā)編程產(chǎn)生的各種頭疼問題,但是卻帶來另一個難題:如何把一個大流程切割成很多獨立小流程?在編碼時,這需要程序員解析函數(shù)流程的所有上下文信息,設(shè)計數(shù)據(jù)結(jié)構(gòu)單獨存儲,并建立關(guān)聯(lián)這些數(shù)據(jù)結(jié)構(gòu)的特殊事件。這往往會帶來幾倍的額外代碼量和工作量。
這個問題幾乎在所有事件驅(qū)動型軟件中都會存在,但在嵌入式軟件中尤為突出。這是因為嵌入式環(huán)境下的CPU、線程等資源受限,而實時性要求高,并發(fā)編程受限。
C++20語言給這個問題提供了一種新的解決方案:協(xié)程。
關(guān)于協(xié)程(coroutine)是什么,在wikipedia[1]等資料中有很好的介紹,本文就不贅述了。在C++20中,協(xié)程的關(guān)鍵字只是語法糖:編譯器會將函數(shù)執(zhí)行的上下文(包括局部變量等)打包成一個對象,并讓未執(zhí)行完的函數(shù)先返回給調(diào)用者。之后,調(diào)用者使用這個對象,可以讓函數(shù)從原來的“斷點”處繼續(xù)往下執(zhí)行。
使用協(xié)程,編碼時就不再需要費心費力的去把函數(shù)“切割”成多個小任務(wù),只用按照習(xí)慣的流程寫函數(shù)內(nèi)部代碼,并在允許暫時中斷執(zhí)行的地方加上co_yield語句,編譯器就可以將該函數(shù)處理為可“分段執(zhí)行”。
協(xié)程用起來的感覺有點像線程切換,因為函數(shù)的棧幀(stack frame)被編譯器保存成了對象,可以隨時恢復(fù)出來接著往下運行。但是實際執(zhí)行時,協(xié)程其實還是單線程順序運行的,并沒有物理線程切換,一切都只是編譯器的“魔法”。所以用協(xié)程可以完全避免多線程切換的性能開銷以及資源占用,也不用擔(dān)心數(shù)據(jù)競爭等問題。
可惜的是,C++20標(biāo)準(zhǔn)只提供了協(xié)程基礎(chǔ)機制,并未提供真正實用的協(xié)程庫(在C++23中可能會改善)。目前要用協(xié)程寫實際業(yè)務(wù)的話,可以借助開源庫,比如著名的cppcoro[2]。然而對于本文所述的場景,cppcoro也沒有直接提供對應(yīng)的工具(generator經(jīng)過適當(dāng)?shù)陌b可以解決這個問題,但是不太直觀),因此我自己寫了一個切割任務(wù)的協(xié)程工具類用于示例。
下面是我寫的SegmentedTask工具類的代碼。這段代碼看起來相當(dāng)復(fù)雜,但是它作為可重用的工具存在,沒有必要讓程序員都理解它的內(nèi)部實現(xiàn),一般只要知道它怎么用就行了。SegmentedTask的使用很容易:它只有3個對外接口:Resume、IsFinished和GetReturnValue,其功能可根據(jù)接口名字自解釋。
#include <optional> #include <coroutine> template<typename T> class SegmentedTask { public: struct promise_type { SegmentedTask<T> get_return_object() { return SegmentedTask{Handle::from_promise(*this)}; } static std::suspend_never initial_suspend() noexcept { return {}; } static std::suspend_always final_suspend() noexcept { return {}; } std::suspend_always yield_value(std::nullopt_t) noexcept { return {}; } std::suspend_never return_value(T value) noexcept { returnValue = value; return {}; } static void unhandled_exception() { throw; } std::optional<T> returnValue; }; using Handle = std::coroutine_handle<promise_type>; explicit SegmentedTask(const Handle coroutine) : coroutine{coroutine} {} ~SegmentedTask() { if (coroutine) { coroutine.destroy(); } } SegmentedTask(const SegmentedTask&) = delete; SegmentedTask& operator=(const SegmentedTask&) = delete; SegmentedTask(SegmentedTask&& other) noexcept : coroutine(other.coroutine) { other.coroutine = {}; } SegmentedTask& operator=(SegmentedTask&& other) noexcept { if (this != &other) { if (coroutine) { coroutine.destroy(); } coroutine = other.coroutine; other.coroutine = {}; } return *this; } void Resume() const { coroutine.resume(); } bool IsFinished() const { return coroutine.promise().returnValue.has_value(); } T GetReturnValue() const { return coroutine.promise().returnValue.value(); } private: Handle coroutine; };
自己編寫協(xié)程的工具類不光需要深入了解C++協(xié)程機制,而且很容易產(chǎn)生懸空引用等未定義行為。因此強烈建議項目組統(tǒng)一使用編寫好的協(xié)程類。如果讀者想深入學(xué)習(xí)協(xié)程工具的編寫方法,可以參考Rainer Grimm的博客文章[3]。
接下來,我們使用SegmentedTask來改造前面的事件處理代碼。當(dāng)一個C++函數(shù)中使用了co_await、co_yield、co_return中的任何一個關(guān)鍵字時,這個函數(shù)就變成了協(xié)程,其返回值也會變成對應(yīng)的協(xié)程工具類。在示例代碼中,需要內(nèi)層函數(shù)提前返回時,使用的是co_yield。但是C++20的co_yield后必須跟隨一個表達式,這個表達式在示例場景下并沒必要,就用了std::nullopt讓其能編譯通過。實際業(yè)務(wù)環(huán)境下,co_yield可以返回一個數(shù)字或者對象用于表示當(dāng)前任務(wù)執(zhí)行的進度,方便外層查詢。
協(xié)程不能使用普通return語句,必須使用co_return來返回值,而且其返回類型也不直接等同于co_return后面的表達式類型。
enum class EventType { COMMAND, MESSAGE, ALARM }; std::vector<EventType> g_events{EventType::COMMAND, EventType::ALARM}; std::optional<SegmentedTask<int>> suspended; // 沒有執(zhí)行完的任務(wù)保存在這里 SegmentedTask<int> ProcessCmd() { for (int i{0}; i < 10; ++i) { std::cout << "Processing step " << i << std::endl; co_yield std::nullopt; } co_return 0; } void ProcessMsg() { std::cout << "Processing Message" << std::endl; } void ProcessAlm() { std::cout << "Processing Alarm" << std::endl; } int main() { for (auto event : g_events) { switch (event) { case EventType::COMMAND: suspended = ProcessCmd(); break; case EventType::MESSAGE: ProcessMsg(); break; case EventType::ALARM: ProcessAlm(); break; } } while (suspended.has_value() && !suspended->IsFinished()) { suspended->Resume(); } if (suspended.has_value()) { std::cout << "Final return: " << suspended->GetReturnValue() << endl; } return 0; }
出于讓示例簡單的目的,事件隊列中只放入了一個COMMAND和一個ALARM,COMMAND是可以分段執(zhí)行的協(xié)程,執(zhí)行完第一段后,主循環(huán)會優(yōu)先執(zhí)行隊列中剩下的事件,最后再來繼續(xù)執(zhí)行COMMAND余下的部分。實際場景下,可根據(jù)需要靈活選擇各種調(diào)度策略,比如專門用一個隊列存放所有未執(zhí)行完的分段任務(wù),并在空閑時依次執(zhí)行。
本文中的代碼使用gcc 10.3版本編譯運行,編譯時需要同時加上-std=c++20和-fcoroutines兩個參數(shù)才能支持協(xié)程。代碼運行結(jié)果如下:
Processing step 0 Processing Alarm Processing step 1 Processing step 2 Processing step 3 Processing step 4 Processing step 5 Processing step 6 Processing step 7 Processing step 8 Processing step 9 Final return: 0
可以看到ProcessCmd函數(shù)(協(xié)程)的for循環(huán)語句并沒有一次執(zhí)行完,在中間插入了ProcessAlm的執(zhí)行。如果分析運行線程還會發(fā)現(xiàn),整個過程中并沒有物理線程的切換,所有代碼都是在同一個線程上順序執(zhí)行的。
使用了協(xié)程的順序圖變成了這樣:
事件處理函數(shù)的執(zhí)行時間長不再是問題,因為可以中途“插入”其他的函數(shù)運行,之后再返回斷點繼續(xù)向下運行。
一個較普遍的認識誤區(qū)是:使用多線程可以提升軟件性能。但事實上,只要CPU沒有空跑,那么當(dāng)物理線程數(shù)超過了CPU核數(shù),就不再會提升性能,相反還會由于線程的切換開銷而降低性能。大多數(shù)開發(fā)實踐中,并發(fā)編程的主要好處并非為了提升性能,而是為了編碼的方便,因為現(xiàn)實中的場景模型很多都是并發(fā)的,容易直接對應(yīng)成多線程代碼。
協(xié)程可以像多線程那樣方便直觀的編碼,但是同時又沒有物理線程的開銷,更沒有互斥、同步等并發(fā)編程中令人頭大的設(shè)計負擔(dān),在嵌入式應(yīng)用等很多場景下,常常是比物理線程更好的選擇。
相信隨著C++20的逐步普及,協(xié)程將來會得到越來越廣泛的使用。
到此,關(guān)于“C++20協(xié)程的使用方法”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
本文標(biāo)題:C++20協(xié)程的使用方法
分享鏈接:http://aaarwkj.com/article22/ggpejc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供ChatGPT、企業(yè)網(wǎng)站制作、手機網(wǎng)站建設(shè)、電子商務(wù)、外貿(mào)網(wǎng)站建設(shè)、網(wǎng)站內(nèi)鏈
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)