C++-一篇文章入門coroutines協程

文章目錄

  • 前言
  • 什么是協程
  • 協程實現原理
  • C++協程的最小例子
      • 1
      • 2
      • 3
      • 4
      • 5
      • 協程等效代碼
  • 協程傳值的例子

前言

最近學習了一下C++協程,這篇文章將介紹協程的相關概念,以及在C++中如何使用協程。

什么是協程

C++中,協程(coroutines)可以理解為一個可以暫停和恢復執行的函數。什么意思呢?例如有以下協程函數:

Task taskFunc()
{...co_await doSomething();  // 1doSomething2();			 // 2
}int main() {auto task = taskFunc(); // 3...						// 4task.resume();			// 5
}

3處調用協程函數,在執行到1的時候(例如doSomething()異步請求網絡資源),函數暫停執行,代碼走到4處,等后面某個合適的時機5,恢復執行協程函數,函數從2繼續執行,對數據進行處理,執行順序是3->1->4->5->2,在這個過程中,執行的線程始終是同一個(除非有意讓協程在不同線程中切換執行)。

協程實現原理

C++中,協程是通過編譯器來實現的,編譯器根據代碼中的co_return/co_await關鍵字,識別一個函數為協程函數,協程函數的返回值類型有如下要求:返回值是一個類,類中包含一個名稱為promise_type的類型(必須是這個類名),promise_type必須實現幾個函數:

  • get_return_object
  • initial_suspend
  • final_suspend
  • unhandled_exception
  • return_void或者return_value
    通常來說,這個外層的類會有一個std::coroutine_handle\<promise_type\> handle成員保存協程的句柄,用于控制協程和獲取協程的相關數據。編譯器會將協程狀態和協程中的數據保存在堆上,并生成執行協程所需的代碼。

C++協程的最小例子

#include <iostream>
#include <coroutine>template <bool READY>
struct Awaiter {bool await_ready() noexcept {std::cout << "await_ready: " << READY << std::endl;return READY;}void await_resume() noexcept {std::cout << "await_resume" << std::endl;}void await_suspend(std::coroutine_handle<>) noexcept {std::cout << "await_suspend" << std::endl;}
};struct TaskPromise {struct promise_type {TaskPromise get_return_object() {std::cout << "get_return_object" << std::endl;return TaskPromise{std::coroutine_handle<promise_type>::from_promise(*this)};}Awaiter<true> initial_suspend() noexcept {std::cout << "initial_suspend" << std::endl;return {};}Awaiter<true> final_suspend() noexcept {std::cout << "final_suspend" << std::endl;return {};}void unhandled_exception() {std::cout << "unhandled_exception" << std::endl;}void return_void() noexcept {std::cout << "return_void" << std::endl;}};void resume() {std::cout << "resume" << std::endl;handle.resume();}std::coroutine_handle<promise_type> handle;
};TaskPromise task_func() {std::cout << "task first run" << std::endl;co_await Awaiter<false>{};std::cout << "task resume" << std::endl;
}int main() {auto promise = task_func();  // 1promise.resume();return 0;
}

接下來我們一步一步解析協程的執行過程:

1

首先task_func就是如前所述的協程函數,因為它包含了一個co_await關鍵字,返回值是TaskPromise類型,其包含了一個promist_type類型,實現了一些必須的函數,滿足協程的所有要求。

當一個協程被調用時,編譯器不會像普通函數一樣在棧上分配空間。相反,它會在堆上動態分配一塊內存,這塊內存被稱為“協程幀”。這個幀里包含了:

  • 協程的 Promise 對象 (promise_type)
  • 所有按值傳遞的參數的拷貝
  • 所有在 co_await 掛起點之間需要保持狀態的局部變量
  • 協程當前執行到的位置(狀態機的狀態)

總結來說就是,第一次執行協程函數task_func時,編譯器會在堆上創建一個協程幀用來保存協程狀態和局部變量等信息。接著,調用promise_type的get_return_object方法,該函數的返回值必須和協程函數的返回值一致,在task_func暫停/結束執行時,返回值就是get_return_object的返回值。

get_return_object函數的實現通常是返回外層類對象,并使用promise_type對象初始化coroutine_handle:

std::coroutine_handle<promise_type>::from_promise(*this)

std::coroutine_handle可以理解為類似std::thread的類,是一個wrapper,用來控制協程和獲取協程數據的。from_promise將一個promise_type類型轉換為std::coroutine_handle,相應的std::coroutine_handle::promise方法用于獲取handle中的promise_type對象。

2

接著,執行的是initial_suspend,這個函數的返回值是一個類類型,通常稱為awaiter,這個類用于控制接下來要繼續執行協程函數,還是暫停執行,這個類必須實現如下函數:await_ready,await_suspend,await_resume,關于這三個函數的詳細解釋,我們等到co_await時再說。
initial_suspend返回類型可以使用stl提供的兩種awaiter:std::suspend_always和std::suspend_never,分別表示總是掛起或者總是繼續執行。其實現也很簡單:

// STRUCT suspend_always
struct suspend_always {_NODISCARD constexpr bool await_ready() const noexcept {return false;}constexpr void await_suspend(coroutine_handle<>) const noexcept {}constexpr void await_resume() const noexcept {}
};

這樣就不用自己實現awaiter了,在我們的例子中,我們實現了自己的awaiter類,并且await_ready返回true,此時會立即調用await_resume。

3

接著開始真正執行task_func函數的第一行

std::cout << "task first run" << std::endl;

然后我們遇到了co_await表達式,co_await后面跟著的是awaiter對象,我們詳細的介紹一下awaiter的三個函數:

  • await_ready。沒有參數,返回bool類型,表示接下來應該繼續執行(true)還是暫停執行(false)
  • await_suspend。當await_ready返回false時,表示沒有準備好數據,此時下一個執行的函數就是await_suspend。await_suspend參數是協程句柄類型std::coroutine_handle,這個參數是編譯器幫忙傳入的。await_suspend返回值可以是void,也可以是bool(如果返回false則又會繼續執行協程),甚至可以是其他協程句柄,從而執行其他協程(這是高級話題,我們以后再說)
  • await_resume。當await_ready返回true時,表示已經準備好數據,此時下一個執行的函數就是await_resume。await_resume沒有參數,返回值可以是任意類型,這個返回值會作為co_return表達式的返回值。

在我們的例子中,co_await Awaiter<false>{};會讓協程掛起,于是編譯器將當前表達式生成的awaiter對象保存在協程幀中,然后協程函數task_func返回一個TaskPromise對象(由前面所說的get_return_object構造),回到了main中執行。

4

接著我們調用了TaskPromise::resume,這個函數只做了一件事,就是調用coroutine_handle的resume方法,它會調用之前co_wait掛起時保存的awaiter對象的resume方法,然后繼續執行協程函數:

std::cout << "task resume" << std::endl;

5

最后,協程函數結束執行,我們沒有寫co_return,編譯器會默認補上co_return在最后,co_return會調用promise_type::return_void()函數,表示沒有返回值。如果有返回值,就需要定義一個叫void return_value(T t)的函數,return_value和return_void不能共存。
接著編譯器調用promise_type::final_suspend結束協程,final_suspend類似initial_suspend,如果掛起,協程不會立即銷毀內部的狀態信息,反之則會立即銷毀,因為我們可能還有部分信息存在promise_type對象中,所以在finial_suspend掛起后,則需要手動釋放coroutine_handle的資源,可以采用RAII的方式,在外層類中的析構函數釋放coroutine_handle:

~TaskPromise()
{handle.destroy();
}

如果發生異常,則會調用promise::unhandled_exception。

協程等效代碼

綜上,我們可以寫出task_func協程執行過程的偽代碼:

TaskPromise task_func() {// No parameters and local variables.auto state = new __TaskPromise_state_(); // has TaskPromise::promise_type promise; TaskPromise coro = state.promise.get_return_object();try {co_await p.inital_suspend();std::cout << "task first run" << std::endl;co_await Awaiter<false>{};std::cout << "task resume" << std::endl;} catch (...) {state.promise.unhandled_exception();}co_await state.promise.final_suspend();
}

協程傳值的例子

下面是一個模擬通過協程獲取數據,最終返回在main中取數據的例子


#include <iostream>
#include <coroutine>
#include <future>
#include <thread>struct TaskPromise {struct promise_type {TaskPromise get_return_object() {std::cout << "get_return_object(), thread_id: " << std::this_thread::get_id() << std::endl;return TaskPromise{std::coroutine_handle<promise_type>::from_promise(*this)};}std::suspend_always initial_suspend() noexcept { return {}; }std::suspend_always final_suspend() noexcept { return {}; }void unhandled_exception() {}void return_void() noexcept {}size_t data = 0;};std::coroutine_handle<promise_type> handle;
};struct Awaiter {bool await_ready() noexcept {std::cout << "await_ready(), thread_id: " << std::this_thread::get_id() << std::endl;return false;}void await_suspend(std::coroutine_handle<TaskPromise::promise_type> handle) noexcept {std::cout << "await_suspend(), thread_id: " << std::this_thread::get_id() << std::endl;auto thread = std::thread([=]() {std::this_thread::sleep_for(std::chrono::seconds(1));handle.promise().data = 1;handle.resume();});thread.join();}void await_resume() noexcept {std::cout << "await_resume(), thread_id: " << std::this_thread::get_id() << std::endl;}
};TaskPromise task_func() {std::cout << "task_func() step 1, thread_id: " << std::this_thread::get_id() << std::endl;co_await Awaiter{};std::cout << "task_func() step 2, thread_id: " << std::this_thread::get_id() << std::endl;
}int main() {std::cout << "main(), thread_id: " << std::this_thread::get_id() << std::endl;auto promise = task_func();std::cout << "main(), data: " << promise.handle.promise().data << ", thread_id: " << std::this_thread::get_id() << std::endl;promise.handle.resume();std::cout << "main(), data: " << promise.handle.promise().data << ", thread_id: " << std::this_thread::get_id() << std::endl;return 0;
}

參考:1. https://mp.weixin.qq.com/s/0njDHtz_SGPkrr4ndAWHaA

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/91124.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/91124.shtml
英文地址,請注明出處:http://en.pswp.cn/web/91124.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

數字經濟專業的就業全景指南

CDA數據分析師證書含金量高&#xff0c;適應了未來數字化經濟和AI發展趨勢&#xff0c;難度不高&#xff0c;行業認可度高&#xff0c;對于找工作很有幫助。一、數字經濟就業熱力圖二、核心崗位發展路徑1. 互聯網數字運營崗2. 金融科技崗崗位類型技能組合證書加持5年薪資范圍智…

PDF轉Word免費工具!批量處理PDF壓縮,合并, OCR識別, 去水印, 簽名等全功能詳解

大家好&#xff0c;歡迎來到程序視點&#xff01;我是你們的老朋友.小二&#xff01;前言PDF軟件我發的非常多&#xff0c;但今天這款工具是大家公認最值得推薦的&#xff0c;這款軟件就是PDF24PDF24幾乎包含了PDF的所有功能&#xff0c;目前是更新到了最新版本&#xff01;文末…

Flutter開發實戰之Widget體系與布局原理

第3章:Widget體系與布局原理 在前面兩章中,我們已經搭建好了Flutter開發環境,并且了解了Dart語言的基礎知識。現在是時候深入Flutter的核心——Widget體系了。如果說Dart是Flutter的語言基礎,那么Widget就是Flutter的靈魂。理解Widget體系,是掌握Flutter開發的關鍵所在。…

C++:stack與queue的使用

stack與queue的使用一.stack與queuej基礎1.stack1.1基本認識1.2示例代碼代碼功能解析2.queue2.1基礎知識操作說明2.2示例代碼代碼分析 一.stack與queuej基礎 1.stack 1.1基本認識以上圖片展示了棧&#xff08;stack&#xff09;這種數據結構的基本操作示意。棧是一種遵循后進先…

Unity 編輯器開發 之 Excel導表工具

一個簡單的Excel導表工具&#xff0c;可以用來熱更數據配置工具使用&#xff1a;&#xfeff;&#xfeff;執行菜單 SDGSupporter/Excel/1.Excel2Cs 生成c#腳本。&#xfeff;&#xfeff;等待C#類編譯完成&#xfeff;&#xfeff;執行菜單 SDGSupporter/Excel/2.Excel2Bytes …

【數據結構與算法】力扣 415. 字符串相加

題目描述 415. 字符串相加 給定兩個字符串形式的非負整數 num1 和num2 &#xff0c;計算它們的和并同樣以字符串形式返回。 你不能使用任何內建的用于處理大整數的庫&#xff08;比如 BigInteger&#xff09;&#xff0c; 也不能直接將輸入的字符串轉換為整數形式。 示例 1…

進階向:Manus AI與多語言手寫識別

Manus AI與多語言手寫識別:從零開始理解 手寫識別技術作為人工智能領域的重要應用之一,近年來在智能設備、教育、金融等行業得到了廣泛運用。根據市場調研機構IDC的數據顯示,2022年全球手寫識別市場規模已達到45億美元,預計到2025年將突破70億美元。其中,多語言手寫識別技…

Javaweb————HTTP請求頭屬性講解

??????????????????????前面我們已經說過http請求分為三部分&#xff0c;請求行&#xff0c;請求頭和請求體 請求頭包含若干個屬性&#xff1a;格式為屬性名&#xff1a;屬性值&#xff0c;這篇文章我們就來介紹一下http請求頭中一些常見屬性的含義 我們…

9.c語言常用算法

查找順序查找&#xff08;線性查找&#xff09;算法思想&#xff1a;從數組的第一個元素開始&#xff0c;逐個與目標值進行比較&#xff0c;直到找到目標值或查找完整個數組。時間復雜度&#xff1a;最好情況&#xff1a;O(1)&#xff08;目標在第一個位置&#xff09;最壞情況…

AI小智源碼分析——音頻部分(一)

一、源碼跳轉這里采用了函數重載來進行代碼復用&#xff0c;當需要對I2S接口的數據進行配置&#xff0c;比如左右音道切換&#xff0c;可以使用第二個構造函數&#xff0c;這里小智使用的是第一個構造函數&#xff0c;即只傳遞I2S相關的引腳參數&#xff08;不帶slot mask&…

【GNSS原理】【LAMBDA】Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法[2025年7月]

Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法 作者&#xff1a;齊花Guyc(CAUC) 文章目錄Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法一.整周模糊度理論1.LAMBDA算法干了一件什么事情&#xff1f;2.LAMBDA算法步驟&#xff08;1&#xff09;去相關&#xff08;Z變換…

計算機畢業設計java在線二手系統的設計與實現 基于Java的在線二手交易平臺開發 Java技術驅動的二手物品管理系統

計算機畢業設計java在線二手系統的設計與實現z2n189&#xff08;配套有源碼 程序 mysql數據庫 論文&#xff09; 本套源碼可以在文本聯xi,先看具體系統功能演示視頻領取&#xff0c;可分享源碼參考。隨著互聯網技術的飛速發展&#xff0c;二手交易市場也逐漸從傳統的線下模式轉…

如何進行項目復盤?核心要點分析

進行項目復盤需要明確復盤目標、確定復盤參與人員、選擇合適的復盤方法、梳理項目過程與關鍵節點、分析成功與失敗的原因、總結經驗教訓并制定改進計劃。其中&#xff0c;選擇合適的復盤方法尤其關鍵&#xff0c;常見的復盤方法包括魚骨圖分析法、SWOT分析法、PDCA循環法&#…

LeetCode 923.多重三數之和

給定一個整數數組 arr &#xff0c;以及一個整數 target 作為目標值&#xff0c;返回滿足 i < j < k 且 arr[i] arr[j] arr[k] target 的元組 i, j, k 的數量。 由于結果會非常大&#xff0c;請返回 109 7 的模。 示例 1&#xff1a; 輸入&#xff1a;arr [1,1,2,2,…

.Net日志系統Logging-五

日志概念 日志級別 NET (Microsoft.Extensions.Logging) 中定義的 6 個標準日志級別&#xff0c;按嚴重性從低到高排列&#xff1a; 日志級別數值描述典型使用場景Trace0最詳細的信息&#xff0c;包含敏感數據&#xff08;如請求體、密碼哈希等&#xff09;。僅在開發或深度故…

中國貿促會融媒體中心出海活動負責人、出海星球創始人蒞臨綠算技術

近日&#xff0c;中國貿促會融媒體中心出海活動負責人、出海星球創始人王思諾一行蒞臨廣東省綠算技術有限公司&#xff0c;深入考察其核心技術產品與全球化布局。雙方圍繞綠算技術全棧產品體系、創新出海模式及生態共建展開深度對話。綠算技術作為國內智算基礎設施領域的領軍企…

【算法專題訓練】06、數組雙指針

1、數組 數組是由相同類型的元素組成的數據集合&#xff0c;并且占據一塊連續的內存&#xff0c;按照順序存儲數據。 1.1、數組的特性&#xff1a; 數組元素通過下標獲取數據數組對象初始化時&#xff0c;需要先指定數組容量大小&#xff0c;并根據容量大小分配內存。缺點&…

操作系統-lecture2(操作系統結構)

回顧下lecture1 swap區域不可以馬上執行&#xff0c;即虛擬內存的數據和指令不可以被執行&#xff0c;得交換回到內存區域 操作系統的服務 主要提供兩種服務 面向普通用戶&#xff1a;user interface面向程序員&#xff1a;應用級程序代碼 為用戶 為用戶提供了操作包括但不…

內網服務器實現從公網穿透

從6月份tplink的ddns失效之后&#xff0c;對于部分在內網運行的服務器&#xff0c;從公網訪問就收到了部分影響。有好幾個朋友找來&#xff0c;尋求幫助&#xff0c;看看怎么恢復原來的機制&#xff0c;可以從公網互聯網訪問內網服務器。方案一&#xff1a;如果有動態公網的客戶…

vcs-編譯+仿真+dump波形【IMP】

VCS仿真分為兩步式(編譯/compilation仿真/simulation)和三步式(分析/analysis細化/elaborationsimulation/仿真);注2:analysis/分析是三步式flow中仿真design的第一步&#xff0c;在此階段將使用vhdlan或vlogan分析VHDL、Verilog、SystemVerilog和OpenVera文件。下面的部分包括…