考慮下面的協程代碼
#include <iostream>
#include <coroutine>
using namespace std;
class Resumable
{
};
Resumable func() {
cout << "hello";
co_await std::suspend_always();
cout << " world";
}
int main()
{
}
編譯報錯
error: unable to find the promise type for this coroutine
13 | co_await std::suspend_always();
| ^~~~~~~~
為什麼?
其實編譯器在編譯時,會希望生成如下的代碼:
/* 經過編譯器優化後的 func 函數 */
Resumable func()
{
Frame *frame = operator new(size); // size = 函數形參大小 + 局部變量大小
Rumable::promise_type promise;
coroutine_handle *handle = coroutine_handle<>::from_promise(&promise);
Resumable res = promise.get_return_object(); // call the Resumable constructor
co_await promise.initial_suspend(); // in some ways, this is a coroutine constructor
try {
// func-body
cout << "hello";
co_await std::suspend_always();
cout << " world";
// func-body end
}catch (...) {
promise.unhandled_exception(); // coroutine exception handle
}
co_await promise.final_suspend(); // in some ways, this is a coroutine destructor
return res;
}
通過上面的代碼,可以引出兩個問題:
- 已知協程co_await可以完成上下文切換,那這個函數中co_await具體是怎麼調用的?
- promise_type 哪裡來?
- Resubmable如何實作?
同樣,從上面的代碼中可以推出,promise_type至少應該含有以下代碼:
class promise_type
{
public:
auto get_return_object();
auto initial_suspend();
void unhandled_exception();
auto final_suspend();
void return_void();
};
抱着上面三個問題,看看Resumable的實作規範。
Resumable的編譯實作
class Resumable
{
public: /* 使用者自定義實作部分 */
class promise_type; // 見上個代碼塊
/*
使用者的其他自定義實作代碼
*/
};
解決上面提出的問題:
- 已知協程co_await可以完成上下文切換,那這個函數中co_await具體是怎麼調用的?
先繼續存疑
- promise_type 哪裡來?
答:從 Resumable 中由使用者手動定義而來,且必須實作一些特定方法。
- Resubmable如何實作?
答:Resumable 必須包含 promise_type 子類型(typedef也算),其餘沒什麼講究。
再提出一些新問題:
- 協程如何将一個值從函數内
到函數外?co_await
-
看起來Resumable在編譯優化後的func裡沒有被用到,隻在最後傳回的時候return了一下,為什麼不用promise直接代替Resumable?
換句話說:為什麼要給promise加一層外套作為傳回類型?C++為什麼要這樣設計?
以下是未解決問題清單:
- 已知協程co_await可以完成上下文切換,那這個函數中co_await具體是怎麼調用的?
- C++為什麼要采用給promise加一層外套作為傳回類型這樣的設計方式?
總結一下
從上面可以看出,co_await 之類的協程關鍵字依然存在,這說明此處的編譯優化并不是針對協程的,那為什麼要這樣做呢?
答案是為了更好的管理協程,可以看到,一次小小的協程函數調用覆寫了誕生、運作、錯誤處理、消亡等各個部分,這為将來高可用的架構奠定了基礎,但對于寫hello world的人不得不說,真***複雜。
C++有一個設計規範,叫做一個人隻做一件事,在這裡promise_type用來管理協程的生命周期。Resumable用來作為傳回值。
如果說上面講的都是協程規範的話,那麼接下來要講的部分就是具體協程的實作,看看 co_await 到底是如何調用的?
Awaitable對象的實作規範
回到最初的起點:
Resumable func() {
cout << "hello";
co_await std::suspend_always();
cout << " world";
}
這裡
co_await std::suspend_always();
調用的是标準庫
coroutine
中的函數,直接來看看他的實作:
// 17.12.5 Trivial awaitables
/// [coroutine.trivial.awaitables]
struct suspend_always
{
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
這就是
co_await expr
的通用标準實作,寫的再通俗點就是:
class Awaiter
{
public:
bool await_ready();
auto await_suspend(coroutine_handle<> handle);
auto await_resume();
};
-
:該任務是否已經完成?若未完成則将調用await_ready
await_suspend
-
await_suspend
:是否中止該任務?若中止則該協程将調用權傳回給 caller 。
該函數傳回bool、void、coro_handle三種之一,對于不同的傳回值編譯器提供了不同的實作。
-
:程式内部調用void
以繼續運作協程。handle.resume()
-
:傳回true表示同意中止,否則繼續執行bool
-
:調用該handle的resume,随後調用權傳回 callerstd::coroutine_handle<>
-
-
:用于傳回協程值,可以是任意類型await_resume
編譯器會通過以下兩種路徑對 await 語句進行優化:
(下面我用僞代碼表示了
await_suspend
在不同傳回值下的編譯代碼)
在調用co_await 等協程關鍵字的位置,程式的操作大概類似這樣:
if(!a.await_ready()) {
# if await_suspend_return_void
try {
result = a.await_suspend(handle);
# if !(await_suspend_return_bool && await_suspend_return_coroutine_handle)
return_to_caller();
#endif
} catch(...) {
excpetion = std::current_exception();
goto resume_point;
}
#elif await_suspend_return_bool
if(!result)
goto resume_point;
return_to_caller();
#elif await_suspend_return_coroutine_handle
result.resume();
return_to_caller();
# endif
resume_point:
if(exception)
std::rethrow_exception(exception);
return a.await_resume();
}
到這裡,編譯器完成了對co_await關鍵字的優化,接下來看個執行個體。
range
衆所周知,python裡有這樣一個函數可以這樣用:
for i in range(0, 10):
print(i, end=' ')
Out:
0 1 2 3 4 5 6 7 8 9
這個函數本質上可以用 python 協程這樣實作:
def my_range(low, high):
print('are you ok?')
while low < high:
yield low
low += 1
iter = my_range(0, 10)
while True:
try:
print(iter.send(None))
except StopIteration:
break
Out:
0 1 2 3 4 5 6 7 8 9
如果用CPP20呢?首先寫出來自己想要的執行代碼,然後再考慮如何實作,說得高大上點,用測試驅動開發。
我們最終想要的是這樣的效果:
int main()
{
Resumable iter = range(low, high);
while(true) {
try{
cout << iter.get() << " ";
iter.resume();
}catch (...) {
break;
}
}
}
具體實作:
#include <coroutine>
#include <iostream>
#include <string>
using namespace std;
class Awaiter
{
public:
Awaiter(int val):val(val) { }
bool await_ready() { return false;}
void await_suspend(coroutine_handle<> handle) { }
void await_resume() { }
int val;
};
class Resumable
{
public:
class promise_type
{
public:
auto get_return_object() { return Resumable(Handle::from_promise(*this)); }
auto initial_suspend() noexcept { return std::suspend_never(); }
auto final_suspend() noexcept { return std::suspend_never(); }
void unhandled_exception() { throw; }
void return_void() { }
Awaiter await_transform(Awaiter awaiter) {
cur_val = awaiter.val;
return awaiter;
}
int cur_val = 0;
};
typedef coroutine_handle<promise_type> Handle;
Resumable(Handle handle):handle(handle) {}
void resume() { handle.resume(); }
int get() { return handle.promise().cur_val; }
private:
Handle handle;
};
Resumable range(int low, int high)
{
while(low < high) {
co_await Awaiter(low++);
}
}
int main()
{
int low = 0, high = 10;
Resumable iter = range(low, high);
while(true) {
try{
cout.flush() << iter.get() << " ";
iter.resume();
}catch (...) {
break;
}
}
}
Out:
0 1 2 3 4 5 6 7 8 9
當然,我上面為了學習了解是以強行将
awaiter
作了一個産出器,實際上這個工作應該交由
co_yield
來完成,他會調用
promise_type.yield_value(expr)
,可以直接從promise中拿到數值,更為簡便。
具體的例子可參考cpp reference 裡的這篇文章。
HelloWorld的協程實作
考慮下面的代碼:
async_void func()
{
cout << "hello ";
co_await std::suspend_always();
cout << "world" << endl;
}
int main()
{
auto f = func();
f.resume();
}
經過協程優化以後:
async_void coro_func()
{
Frame *frame = operator new(size); // size = 函數形參大小 + 局部變量大小
async_void::promise_type promise;
async_void ret = promise.get_return_objet();
int status = 0;
void resume() {
switch(status) {
case 0:
return f0();
case 1:
return f1();
}
}
void f0() { status = 1; cout << "hello "; }
void f1() { cout << "world" << endl; }
return ret;
}
寫得有點累了,先發這麼多,如果有人看就繼續往下寫。
class async_void
{
public:
class promise_type {
public:
auto get_return_object() { return async_void{Handle::from_promise(*this)}; }
auto initial_suspend() { return std::suspend_never(); }
void unhandled_exception() { throw; }
auto final_suspend() { return std::suspend_never(); }
void return_void() {}
};
typedef std::coroutine_handle<promise_type> Handle;
explicit async_void(Handle h):handle(h) {}
Handle handle;
bool resume() {
if(!handle.done())
handle.resume();
return !handle.done();
}
}
async_void func()
{
cout << "hello ";
co_await std::suspend_always();
cout << "world" << endl;
}
int main()
{
auto coro = func();
while(coro.resume());
}
async_void func()
{
Frame *frame = operator new(size); // size = 函數形參大小 + 局部變量大小
async_void::promise_type promise;
coroutine_handle *handle = coroutine_handle<>::from_promise(&promise);
async_void res = promise.get_return_object(); // call the Resumable constructor
co_await promise.initial_suspend(); // in some ways, this is a coroutine constructor
try {
cout << "hello ";
co_await std::suspend_always();
cout << "world" << endl;
}catch (...) {
promise.unhandled_exception(); // coroutine exception handle
}
co_await promise.final_suspend(); // in some ways, this is a coroutine destructor
return res;
}
auto a = std::suspend_always();
if(!a.await_ready()) {
try {
a.await_suspend(handle);
return_to_caller();
}catch(...) {
exception = std::current_exception();
goto resume_point();
}
}
resume_point:
if(exception)
std::rethrow_exception(exception);
return a.await_resume();
async_void hi() {
cout << "h";
co_await std::suspend_always();
cout << "i";
}
async_void func() {
cout << "hello";
co_await hi().await;
cout << "world";
}
int main() {
auto coro1 = hi();
auto coro2 = func();
coro1.resume();
coro2.resume();
}
參考
- cpp reference
- 阿裡安龍飛的視訊
- 知乎啟蒙帖