日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > c/c++ >内容正文

c/c++

微信终端自研 C++协程框架的设计与实现

發(fā)布時(shí)間:2024/2/28 c/c++ 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 微信终端自研 C++协程框架的设计与实现 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

作者:peterfan,騰訊 WXG 客戶端開發(fā)工程師

背景

基于跨平臺(tái)考慮,微信終端很多基礎(chǔ)組件使用 C++ 編寫,隨著業(yè)務(wù)越來越復(fù)雜,傳統(tǒng)異步編程模型已經(jīng)無法滿足業(yè)務(wù)需要。Modern C++ 雖然一直在改進(jìn),但一直沒有統(tǒng)一編程模型,為了提升開發(fā)效率,改善代碼質(zhì)量,我們自研了一套 C++ 協(xié)程框架 owl,用于為所有基礎(chǔ)組件提供統(tǒng)一的編程模型。

owl 協(xié)程框架目前主要應(yīng)用于 C++ 跨平臺(tái)微信客戶端內(nèi)核(Alita),Alita 的業(yè)務(wù)邏輯部分全部用協(xié)程實(shí)現(xiàn),相比傳統(tǒng)異步編程模型,至少減少了 50% 代碼量。Alita 目前已經(jīng)應(yīng)用于兒童手表微信、Linux 車機(jī)微信、Android 車機(jī)微信等多個(gè)業(yè)務(wù),其中 Linux 車機(jī)微信的所有 UI 邏輯也全部用協(xié)程實(shí)現(xiàn)

為什么要造輪子?

那么問題來了,既然 C++20 已經(jīng)支持了協(xié)程,業(yè)界也有不少開源方案(如 libco、libgo 等),為什么不直接使用?

原因:

  • owl 基礎(chǔ)庫(kù)需要支持盡量多的操作系統(tǒng)和架構(gòu),操作系統(tǒng)包括:Android、iOS、macOS、Windows、Linux、RTOS;架構(gòu)包括:x86、x86_64、arm、arm64、loongarch64,目前并沒有任何一個(gè)方案能直接支持。

  • owl 協(xié)程自 2019 年初就推出了,而當(dāng)時(shí) C++20 還未成熟,實(shí)際上到目前為止 C++20 普及程度依然不高,公司內(nèi)部和外部合作伙伴的編譯器版本普遍較低,導(dǎo)致目前 owl 最多只能用到 C++14 的特性

  • 業(yè)界已有方案有不少缺點(diǎn):

  • 大多為后臺(tái)開發(fā)設(shè)計(jì),不適用終端開發(fā)場(chǎng)景

  • 基本只支持 Linux 系統(tǒng)和 x86/x86_64 架構(gòu)

  • 封裝層次較低,大多是玩具或 API 級(jí)別,并沒有達(dá)到框架級(jí)別

  • 在 C++ 終端開發(fā)沒有看到大規(guī)模應(yīng)用案例

  • Show me the code

    那么協(xié)程比傳統(tǒng)異步編程到底好在哪里?下面我們結(jié)合代碼來展示一下協(xié)程的優(yōu)勢(shì),同時(shí)也回顧一下異步編程模型的演化過程:

    假設(shè)有一個(gè)異步方法 AsyncAddOne,用于將一個(gè) int 值加 1,為了簡(jiǎn)單起見,這里直接開一個(gè)線程 sleep 100ms 后再回調(diào)新的值:

    void?AsyncAddOne(int?value,?std::function<void?(int)>?callback)?{std::thread?t([value,?callback?=?std::move(callback)]?{std::this_thread::sleep_for(100ms);callback(value?+?1);});t.detach(); }

    要調(diào)用 AsyncAddOne 將一個(gè) int 值加 3,有三種主流寫法:

    1、Callback

    傳統(tǒng)回調(diào)方式,代碼寫起來是這樣:

    AsyncAddOne(100,?[]?(int?result)?{AsyncAddOne(result,?[]?(int?result)?{AsyncAddOne(result,?[]?(int?result)?{printf("result?%d\n",?result);});}); });

    回調(diào)有一些眾所周知的痛點(diǎn),如回調(diào)地獄、信任問題、錯(cuò)誤處理困難、生命周期管理困難等,在此不再贅述。

    2、Promise

    Promise 解決了 Callback 的痛點(diǎn),使用 owl::promise 庫(kù)的代碼寫起來是這樣:

    //?將回調(diào)風(fēng)格的?AsyncAddOne?轉(zhuǎn)成?Promise?風(fēng)格 owl::promise?AsyncAddOnePromise(int?value)?{return?owl::make_promise([=]?(auto?d)?{AsyncAddOne(value,?[=]?(int?result)?{d.resolve(result);});}); }//?Promise?方式 AsyncAddOnePromise(100) .then([]?(int?result)?{return?AsyncAddOnePromise(result); }) .then([]?(int?result)?{return?AsyncAddOnePromise(result); }) .then([]?(int?result)?{printf("result?%d\n",?result); });

    很顯然,由于消除了回調(diào)地獄,代碼漂亮多了。實(shí)際上 owl::promise 解決了 Callback 的所有痛點(diǎn),通過使用模版元編程和類型擦除技術(shù),甚至連語(yǔ)法都接近 JavaScript Promise。

    但實(shí)踐發(fā)現(xiàn),Promise 只適合線性異步邏輯,復(fù)雜一點(diǎn)的異步邏輯用 Promise 寫起來也很亂(如循環(huán)調(diào)用某個(gè)異步接口),因此我們廢棄了 owl::promise,最終將方案轉(zhuǎn)向了協(xié)程。

    3、Coroutine

    使用 owl 協(xié)程寫起來是這樣:

    //?將回調(diào)風(fēng)格的?AsyncAddOne?轉(zhuǎn)成?Promise?風(fēng)格 //?注: //?owl::promise?擦除了類型,owl::promise2?是類型安全版本 //?owl?協(xié)程需要配合?owl::promise2?使用 owl::promise2<int>?AsyncAddOnePromise2(int?value)?{return?owl::make_promise2<int>([=]?(auto?d)?{AsyncAddOne(value,?[=]?(int?result)?{d.resolve(result);});}); }//?Coroutine?方式 //?使用?co_launch?啟動(dòng)一個(gè)協(xié)程 //?在協(xié)程中即可使用?co_await?將異步調(diào)用轉(zhuǎn)成同步方式 owl::co_launch([]?{auto?value?=?100;for?(auto?i?=?0;?i?<?3;?i++)?{value?=?co_await?AsyncAddOnePromise2(value);}printf("result?%d\n",?value); });

    使用協(xié)程可以用同步方式寫異步代碼,大大減輕了異步編程的心智負(fù)擔(dān)。co_await 語(yǔ)法糖讓 owl 協(xié)程寫起來跟很多語(yǔ)言內(nèi)置的協(xié)程并無差別。

    回調(diào)轉(zhuǎn)協(xié)程

    要在實(shí)際業(yè)務(wù)中使用協(xié)程,必須通過某種方式讓回調(diào)代碼轉(zhuǎn)換為協(xié)程支持的形式。通過上面的例子可以看出,回調(diào)風(fēng)格接口要支持在協(xié)程中同步調(diào)用非常簡(jiǎn)單,只需短短幾行代碼將回調(diào)接口先轉(zhuǎn)成 Promise 接口,在協(xié)程中即可直接通過 co_await 調(diào)用:

    //?回調(diào)接口 void?AsyncAddOne(int?value,?std::function<void?(int)>?callback);//?Promise?接口 owl::promise2<int>?AsyncAddOnePromise2(int?value);//?協(xié)程中調(diào)用 auto?value?=?co_await?AsyncAddOnePromise2(100);

    實(shí)際項(xiàng)目中通常會(huì)省略掉上述中間步驟,直接一步到位:

    //?在協(xié)程中可以像調(diào)用普通函數(shù)一樣調(diào)用此函數(shù) int?AsyncAddOneCoroutine(int?value)?{return?co_await?owl::make_promise2<int>([=]?(auto?d)?{AsyncAddOne(value,?[=]?(int?result)?{d.resolve(result);});}); }

    后臺(tái)開發(fā)使用協(xié)程,通常會(huì) hook socket 相關(guān)的 I/O API,而終端開發(fā)很少需要在協(xié)程中使用底層 I/O 能力,通常已經(jīng)封裝好了高層次的異步 I/O 接口,因此 owl 協(xié)程并沒有 hook I/O API,而是提供一種方便的將回調(diào)轉(zhuǎn)協(xié)程的方式。

    一個(gè)完整的例子

    上述代碼片段很難體現(xiàn)出協(xié)程的實(shí)際用法,這個(gè)例子使用協(xié)程實(shí)現(xiàn)了一個(gè) tcp-echo-server,只有 40 多行代碼:

    int?main(int?argc,?char*?argv[])?{//?使用?co_thread_scope()?創(chuàng)建一個(gè)協(xié)程作用域,并啟動(dòng)一個(gè)線程作為協(xié)程調(diào)度器co_thread_scope()?{owl::tcp_server?server;int?error?=?server.listen(3090);if?(error?<?0)?{zerror("tcp?server?listen?failed!");return;}zinfo("tcp?server?listen?OK,?local?%_",?server.local_address());while?(true)?{auto?client?=?server.accept();if?(!client)?{zerror("tcp?server?accept?failed!");break;}zinfo("accept?OK,?local?%_,?peer?%_",?client->local_address(),?client->peer_address());//?當(dāng)有新?client?連接時(shí),使用?co_launch?啟動(dòng)一個(gè)協(xié)程專門處理owl::co_launch([client]?{char?buf[1024]?=?{?0?};while?(true)?{auto?num_recv?=?client->recv_some(buf,?sizeof(buf),?0);if?(num_recv?<=?0)?{break;}buf[num_recv]?=?'\0';zinfo("[fd=%_]?RECV?%_?bytes:?%_",?client->fd(),?num_recv,?buf);if?(strcmp(buf,?"exit")?==?0)?{break;}auto?num_send?=?client->send(buf,?num_recv,?0);if?(num_send?<?0)?{break;}zinfo("[fd=%_]?SENT?%_?bytes?back",?client->fd(),?num_send);}});}};return?0; }

    框架分層

    為了便于擴(kuò)展和復(fù)用,owl 協(xié)程采用分層設(shè)計(jì),開發(fā)者可以直接使用最上層的 API,也可以基于 Context API 或 Core API 搭建自己的協(xié)程框架。

    協(xié)程設(shè)計(jì)

    協(xié)程棧

    協(xié)程按有無調(diào)用棧分為兩類:

    • 有棧協(xié)程(stackful):每個(gè)協(xié)程都有自己的調(diào)用棧,類似于線程的調(diào)用棧

    • 無棧協(xié)程(stackless):協(xié)程沒有調(diào)用棧,協(xié)程的狀態(tài)通過狀態(tài)機(jī)或閉包來實(shí)現(xiàn)

    很顯然,無棧協(xié)程比有棧協(xié)程占用更少的內(nèi)存,但無棧協(xié)程通常需要手動(dòng)管理狀態(tài),如果自研協(xié)程采用無棧方式會(huì)非常難用。因此語(yǔ)言級(jí)別的協(xié)程通常使用無棧協(xié)程,將復(fù)雜的狀態(tài)管理交給編譯器處理;自研方案通常使用有棧協(xié)程,owl 也不例外是有棧協(xié)程

    有棧協(xié)程按棧的管理方式又可以分為兩類:

    • 獨(dú)立棧:每個(gè)協(xié)程都有獨(dú)立的調(diào)用棧

    • 共享?xiàng)?/strong>:每個(gè)協(xié)程都有獨(dú)立的狀態(tài)棧,一個(gè)線程中的多個(gè)協(xié)程共享一個(gè)調(diào)用棧。由于這些協(xié)程中同時(shí)只會(huì)有一個(gè)協(xié)程處于活躍狀態(tài),當(dāng)前活躍的協(xié)程可以臨時(shí)使用調(diào)用棧。當(dāng)此協(xié)程被掛起時(shí),將調(diào)用棧中的狀態(tài)保存到自身的狀態(tài)棧;當(dāng)協(xié)程恢復(fù)運(yùn)行時(shí),將狀態(tài)棧再拷貝到調(diào)用棧。實(shí)踐中通常設(shè)置較大的調(diào)用棧和較小的狀態(tài)棧,來達(dá)到節(jié)省內(nèi)存的目的。

    共享?xiàng)1举|(zhì)上是一種時(shí)間換空間的做法,但共享?xiàng)S幸粋€(gè)比較明顯的缺點(diǎn),看代碼:

    owl::co_launch("co1",?[]?{char?buf[1024]?=?{?0?};auto?job?=?owl::co_launch("co2",?[&buf]?{//?oops!!!buf[0]?=?'a';});job->join(); });

    上面的代碼在共享?xiàng)DJ较聲?huì)出問題,協(xié)程 co1 在棧上分配的 buf,在協(xié)程 co2 訪問的時(shí)候已經(jīng)失效了。要規(guī)避共享?xiàng)5倪@個(gè)缺點(diǎn),可能需要對(duì)協(xié)程的使用做一些限制或檢查,無疑會(huì)加重使用者的負(fù)擔(dān)。

    對(duì)于終端開發(fā),由于同時(shí)運(yùn)行的協(xié)程數(shù)量并不多,性能問題并不明顯,為了使用上的便捷性,owl 協(xié)程使用獨(dú)立棧

    選擇獨(dú)立棧之后,協(xié)程棧應(yīng)該如何分配又是另外的問題,有如下幾種方案:

    • Split Stacks:簡(jiǎn)單來說是一個(gè)支持自動(dòng)增長(zhǎng)的非連續(xù)棧,由于只有 gcc 支持且有兼容性問題,實(shí)踐中比較少用

    • malloc/mmap:直接使用 malloc 或 mmap 分配內(nèi)存,業(yè)界主流方案

    • Thread Stack:在線程中預(yù)先分配一大段棧內(nèi)存作為協(xié)程棧,業(yè)界比較少用

    后兩種方案通常還會(huì)采用內(nèi)存池來優(yōu)化性能,采用 mprotect 來進(jìn)行棧保護(hù)

    owl 協(xié)程同時(shí)使用了后兩種方案,那么什么場(chǎng)景下會(huì)使用到 Thread Stack 方案呢?因?yàn)?Android JNI 和部分 RTOS 系統(tǒng)調(diào)用 會(huì)檢查 sp 寄存器是否在線程棧空間內(nèi),如果不在則認(rèn)為棧被破壞,程序會(huì)直接掛掉。獨(dú)立棧協(xié)程在執(zhí)行時(shí) sp 寄存器會(huì)被修改為指向協(xié)程棧,而通過 malloc/mmap 分配的協(xié)程棧空間不屬于任何線程棧,一定無法通過 sp 檢查。為了解決這個(gè)問題,我們?cè)?Android 和部分 RTOS 上默認(rèn)使用 Thread Stack。

    協(xié)程調(diào)度

    協(xié)程按控制傳遞機(jī)制分為兩類:

    • 對(duì)稱協(xié)程(Symmetric Coroutine):和線程類似,協(xié)程之間是對(duì)等關(guān)系,多個(gè)協(xié)程之間可以任意跳轉(zhuǎn)

    • 非對(duì)稱協(xié)程(Asymmetric Coroutine):協(xié)程之間存在調(diào)用和被調(diào)用關(guān)系,如協(xié)程 A 調(diào)用/恢復(fù)協(xié)程 B,協(xié)程 B 掛起/返回時(shí)只能回到協(xié)程 A

    非對(duì)稱協(xié)程與函數(shù)調(diào)用類似,比較容易理解,主流編程語(yǔ)言對(duì)協(xié)程的支持大都是非對(duì)稱協(xié)程。從實(shí)現(xiàn)的角度,非對(duì)稱協(xié)程的實(shí)現(xiàn)也比較簡(jiǎn)單,實(shí)際上我們很容易用非對(duì)稱協(xié)程實(shí)現(xiàn)對(duì)稱協(xié)程。owl 協(xié)程使用非對(duì)稱協(xié)程

    上圖展示了非對(duì)稱協(xié)程調(diào)用和函數(shù)調(diào)用的相似性,詳細(xì)的時(shí)序如下:

  • 調(diào)用者調(diào)用 co_create() 創(chuàng)建協(xié)程,這一步會(huì)分配一個(gè)單獨(dú)的協(xié)程棧,并為 func 設(shè)置好執(zhí)行環(huán)境

  • 調(diào)用者調(diào)用 co_resume() 啟動(dòng)協(xié)程,func 函數(shù)開始運(yùn)行

  • 協(xié)程運(yùn)行到 co_yield(),協(xié)程掛起自己并返回到調(diào)用者

  • 調(diào)用者調(diào)用 co_resume() 恢復(fù)協(xié)程,協(xié)程從 co_yield() 后續(xù)代碼繼續(xù)執(zhí)行

  • 協(xié)程執(zhí)行完畢,返回到調(diào)用者

  • 如上圖所示,有意思的是,如果一個(gè)協(xié)程沒用調(diào)用 co_yield(),這個(gè)協(xié)程的調(diào)用流程其實(shí)跟函數(shù)一模一樣,因此我們經(jīng)常會(huì)說:函數(shù)就是協(xié)程的一種特例

    單線程調(diào)度器

    協(xié)程和線程很像,不同的是線程多是搶占式調(diào)度,而協(xié)程多是協(xié)作式調(diào)度。多個(gè)線程之間共享資源時(shí)通常需要鎖和信號(hào)量等同步原語(yǔ),而協(xié)程可以不需要。

    通過上面的示例可以看出,使用 co_create() 創(chuàng)建協(xié)程后,可以通過不斷調(diào)用 co_resume() 來驅(qū)動(dòng)協(xié)程的運(yùn)行,而協(xié)程函數(shù)可以隨時(shí)調(diào)用 co_yield() 來掛起自己并將控制權(quán)轉(zhuǎn)移給調(diào)用者。

    很顯然,當(dāng)協(xié)程數(shù)量較多時(shí),通過手工調(diào)用 co_resume() 來驅(qū)動(dòng)協(xié)程不太現(xiàn)實(shí),因此需要實(shí)現(xiàn)協(xié)程調(diào)度器。

    協(xié)程調(diào)度器分為兩類:

    • 1:N 調(diào)度(單線程調(diào)度):使用 1 個(gè)線程調(diào)度 N 個(gè)協(xié)程,由于多個(gè)協(xié)程都在同一個(gè)線程中運(yùn)行,因此協(xié)程之間訪問共享資源無需加鎖

    • M:N 調(diào)度(多線程調(diào)度):使用 M 個(gè)線程調(diào)度 N 個(gè)協(xié)程,由于多個(gè)協(xié)程可能不在同一個(gè)線程運(yùn)行,甚至同一個(gè)協(xié)程每次調(diào)度都有可能運(yùn)行在不同線程,因此協(xié)程之間訪問共享資源需要加鎖,且協(xié)程中使用 TLS(Thread Local Storage) 會(huì)有問題

    單線程調(diào)度通常使用 RunLoop 之類的消息循環(huán)來作為調(diào)度器,雖然調(diào)度性能低于多線程調(diào)度,但單線程調(diào)度器可以免加鎖的特性,能極大降低編碼復(fù)雜度,因此 owl 協(xié)程使用單線程調(diào)度

    使用 RunLoop 作為調(diào)度器的原理其實(shí)很簡(jiǎn)單,將所有 co_resume() 調(diào)用都 Post 到 RunLoop 中執(zhí)行即可。原理如圖所示,要想象一個(gè)協(xié)程是如何在 RunLoop 中執(zhí)行的,大概可以認(rèn)為是:協(xié)程函數(shù)中的代碼被 co_yield() 分隔成多個(gè)部分,每一部分代碼都被 Post 到 RunLoop 中執(zhí)行

    使用 RunLoop 作為調(diào)度器除了協(xié)程不用加鎖,還有一些額外的好處:

    • 協(xié)程中的代碼可以和 RunLoop 中的傳統(tǒng)異步代碼和諧共處

    • 若使用 UI 框架的 RunLoop 作為調(diào)度器,從協(xié)程中可以直接訪問 UI

    為了方便擴(kuò)展,owl 協(xié)程將調(diào)度器抽象成一個(gè)單獨(dú)的接口類,開發(fā)者可以很容易實(shí)現(xiàn)自己的調(diào)度器,或和項(xiàng)目已有的 RunLoop 機(jī)制結(jié)合:

    class?executor?{ public:virtual?~executor()?{}virtual?uint64_t?post(std::function<void?()>?closure)?=?0;virtual?uint64_t?post_delayed(unsigned?delay,?std::function<void?()>?closure)?=?0;virtual?void?cancel(uint64_t?id)?{} };

    在 Linux 車機(jī)微信客戶端,我們通過實(shí)現(xiàn)自定義調(diào)度器讓協(xié)程運(yùn)行在 UI 框架的消息循環(huán)中,得以方便地在協(xié)程中訪問 UI。

    協(xié)程間通信

    通過使用單線程調(diào)度器,多個(gè)協(xié)程之間訪問共享資源不再需要多線程的鎖機(jī)制了。

    那么用協(xié)程寫代碼是否就完全不需要加鎖呢?看代碼:

    static?int?value?=?0; for?(auto?i?=?0;?i?<?4;?++i)?{owl::co_launch([]?{value++;owl::co_delay(1000);value--;printf("value?%d\n",?value);}); }

    假設(shè)協(xié)程中要先將 value++,做完一些事情,再將 value--,我們期望最終 4 個(gè)協(xié)程的輸出都是 0。但由于 owl::co_delay(1000) 這一行導(dǎo)致了協(xié)程調(diào)度,最終輸出結(jié)果必然不符合預(yù)期。

    一些協(xié)程庫(kù)為了解決這種問題,提供了和多線程鎖類似的協(xié)程鎖機(jī)制。好不容易避免了線程鎖,又要引入?yún)f(xié)程鎖,難道沒有更好的辦法了嗎?

    實(shí)際上目前主流的并發(fā)模型除了共享內(nèi)存模型,還有 Actor 模型與 CSP(Communicating Sequential Processes)模型,對(duì)比如下:

    Do not communicate by sharing memory; instead, share memory by communicating. 不要通過共享內(nèi)存來通信,而應(yīng)該通過通信來共享內(nèi)存

    相信這句 Go 語(yǔ)言的哲學(xué)大家已經(jīng)不陌生了,如何理解這句話?本質(zhì)上看,多個(gè)線程或協(xié)程之間同步信息最終都是通過共享內(nèi)存來進(jìn)行的,因?yàn)闊o論是用哪種通信模型,最終都是從內(nèi)存中獲取數(shù)據(jù),因此這句話我們可以理解為 盡量使用消息來通信,而不要直接共享內(nèi)存。

    Actor 模型和 CSP 模型采用的都是消息機(jī)制,區(qū)別在于 Actor 模型里協(xié)程與消息隊(duì)列(mailbox)是綁定關(guān)系;而 CSP 模型里協(xié)程與消息隊(duì)列(channel)是獨(dú)立的。從耦合性的角度,CSP 模型比 Actor 模型更松耦合,因此 owl 協(xié)程使用 channel 作為協(xié)程間通信機(jī)制

    由于我們?cè)趯?shí)際業(yè)務(wù)開發(fā)中并沒有遇到一定需要協(xié)程鎖的場(chǎng)景,因此 owl 協(xié)程暫沒有提供協(xié)程鎖機(jī)制。

    結(jié)構(gòu)化并發(fā)

    想象這樣一個(gè)場(chǎng)景:我們寫一個(gè) UI 界面,在這個(gè)界面會(huì)啟動(dòng)若干協(xié)程通過網(wǎng)絡(luò)去拉取和更新數(shù)據(jù),當(dāng)用戶退出 UI 時(shí),為了不泄露資源,我們希望協(xié)程以及協(xié)程發(fā)起的異步操作都能取消。當(dāng)然,我們可以通過手動(dòng)保存每一個(gè)協(xié)程的句柄,在 UI 退出時(shí)通知每一個(gè)協(xié)程退出,并等待所有協(xié)程都結(jié)束后再退出 UI。然而,手動(dòng)進(jìn)行上述操作非常繁瑣,而且很難保證正確性。

    不止是使用協(xié)程才會(huì)遇到上述問題,把協(xié)程換成線程,問題依然存在。傳統(tǒng)并發(fā)主要有兩類問題:

    • 生命周期問題:如何保證協(xié)程引用的資源不被突然釋放?

    • 協(xié)程取消問題:1)如何打斷正在掛起的協(xié)程?2)結(jié)束協(xié)程時(shí),如何同時(shí)結(jié)束協(xié)程中創(chuàng)建的子協(xié)程?3)如何等待所有子協(xié)程都結(jié)束后再結(jié)束父協(xié)程?

    這里的主要矛盾在于:協(xié)程是獨(dú)立的,但業(yè)務(wù)是結(jié)構(gòu)化的

    為了解決這個(gè)問題,owl 協(xié)程引入了結(jié)構(gòu)化并發(fā)

    結(jié)構(gòu)化并發(fā)的概念是:

    • 作用域中的并發(fā)操作,必須在作用域退出前結(jié)束

    • 作用域可以嵌套

    作用域是一個(gè)抽象概念,有明確生命周期的實(shí)體都是作用域,如:

    • 一個(gè)代碼塊

    • 一個(gè)對(duì)象

    • 一個(gè) UI 頁(yè)面

    如上圖所示,代碼由上而下執(zhí)行,在進(jìn)入外部 scope 后,從 scope 中啟動(dòng)了兩個(gè)協(xié)程,并進(jìn)入了內(nèi)部 scope,當(dāng)執(zhí)行流最終從外部 scope 出來時(shí),結(jié)構(gòu)化并發(fā)機(jī)制必須保證這兩個(gè)協(xié)程已經(jīng)結(jié)束。同樣的,若內(nèi)部 scope 中啟動(dòng)了協(xié)程,執(zhí)行流從內(nèi)部 scope 出來時(shí),也必須保證其中的協(xié)程全部結(jié)束。

    結(jié)構(gòu)化并發(fā)在 owl 協(xié)程的實(shí)現(xiàn)其實(shí)并不復(fù)雜,本質(zhì)上是一個(gè)樹形結(jié)構(gòu):

    核心理念是:

    • 協(xié)程也是一個(gè)作用域

    • 協(xié)程有父子關(guān)系

    • 父協(xié)程取消,子協(xié)程也自動(dòng)取消

    • 父協(xié)程結(jié)束前,必須等待子協(xié)程結(jié)束

    光說概念有點(diǎn)抽象,最后來看一個(gè) owl 協(xié)程結(jié)構(gòu)化并發(fā)的例子:

    class?SimpleActivity?{ public:SimpleActivity()?{//?為?scope_?設(shè)置調(diào)度器,后續(xù)通過?scope_?啟動(dòng)的協(xié)程//?默認(rèn)使用?UI?的消息循環(huán)作為調(diào)度器scope_.set_exec(GetUiExecutor());}~SimpleActivity()?{//?UI?銷毀的時(shí)候取消所有子協(xié)程scope_.cancel();//?scope_?析構(gòu)時(shí)會(huì)等待所有子協(xié)程結(jié)束}void?OnButtonClicked()?{//?在?UI?事件中通過?scope_?啟動(dòng)協(xié)程scope_.co_launch([=]?{//?啟動(dòng)子協(xié)程下載圖片auto?p1?=?owl::co_async([]?{?return?DownloadImage(...);?});auto?p2?=?owl::co_async([]?{?return?DownloadImage(...);?});//?等待圖片下載完畢auto?image1?=?co_await?p1;auto?image2?=?co_await?p2;//?合并圖片auto?new_image?=?co_await?AsyncCombineImage(image1,?image2);//?更新圖片,由于協(xié)程運(yùn)行在消息循環(huán)中,可以直接訪問?UIimage_->SetImage(new_image);});//?可以通過?scope_?啟動(dòng)任意多個(gè)協(xié)程scope_.co_launch([=]?{...});}private:owl::co_scope?scope_;ImageLabel*?image_; };

    性能測(cè)試

    說明:

    • 上下文切換:使用 Context API 進(jìn)行上下文切換的性能,耗時(shí)在 20~30ns 級(jí)別

    • 協(xié)程切換:使用單線程調(diào)度器進(jìn)行協(xié)程切換的性能,耗時(shí)在 0.5~3us 級(jí)別

    • 線程切換:pthread 線程切換的性能,耗時(shí)在 2~8us 級(jí)別

    owl 協(xié)程受限于單線程調(diào)度器性能,切換速度和上下文切換比并不算快,但在終端使用也足夠了。

    總結(jié)

    總的來說,自 owl 協(xié)程在實(shí)際項(xiàng)目中應(yīng)用以來,開發(fā)效率和代碼質(zhì)量都有很大提升。owl 協(xié)程雖然已經(jīng)得到廣泛應(yīng)用,但還存在很多不完善的地方,未來會(huì)繼續(xù)迭代打磨。owl 現(xiàn)階段在騰訊內(nèi)部開源,待框架更完善且 API 穩(wěn)定后,再進(jìn)行對(duì)外開源。

    最近熱文

    業(yè)界首創(chuàng),騰訊網(wǎng)絡(luò)平臺(tái)部實(shí)現(xiàn)大規(guī)模光網(wǎng)絡(luò)實(shí)時(shí)管控系統(tǒng)TOOP

    騰訊程序員不尋常的三年

    總結(jié)

    以上是生活随笔為你收集整理的微信终端自研 C++协程框架的设计与实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。