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