C++協(xié)程實現(xiàn)序列生成器的案例分享
目標
序列生成器通常的實現(xiàn)是在一個協(xié)程內(nèi)部通過某種方式向外部傳一個值出去,并且將自己掛起,外部調(diào)用者則可以獲取到這個值,并且在后續(xù)繼續(xù)恢復(fù)執(zhí)行序列生成器來獲取下一個值。
掛起和向外部傳值的任務(wù)就需要通過co_await
來完成,外部獲取值的任務(wù)就要通過協(xié)程的返回值來完成。
程序大概可以寫成下面這樣:
Generator sequence() { int i = 0; while (true) { co_await i++; } } int main() { auto generator = sequence(); for (int i = 0; i < 10; ++i) { std::cout << generator.next() << std::endl; } }
注意到 generator 有個 next 函數(shù),調(diào)用它時我們需要想辦法讓協(xié)程恢復(fù)執(zhí)行,并將下一個值傳出來。
好了,接下來我們就帶著這兩個問題去尋找解決辦法,順便把剩下的一點點 C++ 協(xié)程的知識補齊。
調(diào)用者獲取協(xié)程產(chǎn)生的值
我們觀察main函數(shù)中的這段代碼:
int main() { auto generator = sequence(); for (int i = 0; i < 10; ++i) { std::cout << generator.next() << std::endl; } }
generator
的類型就是我們即將實現(xiàn)的序列生成器類型 Generator
,結(jié)合上一篇文章當(dāng)中對于協(xié)程返回值類型的介紹,我們先大致給出它的定義:
struct Generator { struct promise_type { // 開始執(zhí)行時直接掛起等待外部調(diào)用 resume 獲取下一個值 std::suspend_always initial_suspend() { return {}; }; // 執(zhí)行結(jié)束后不需要掛起 std::suspend_never final_suspend() noexcept { return {}; } // 為了簡單,我們認為序列生成器當(dāng)中不會拋出異常,這里不做任何處理 void unhandled_exception() { } // 構(gòu)造協(xié)程的返回值類型 Generator get_return_object() { return Generator{}; } // 沒有返回值 void return_void() { } }; int next() { ???.resume(); return ???; } };
代碼當(dāng)中有兩處我們標注為 ???,表示暫時還不知道怎么處理。
如果我們想要在Generator
中resume
協(xié)程的話,需要拿到coroutine_handle
,要如何做到這一點呢?
這個時候可以記住一點,promise_type
是鏈接協(xié)程內(nèi)外的橋梁,想要拿到什么,找promise_type
要。標準庫提供了一個通過promise_type
的對象的地址獲取coroutine_handle
的函數(shù),它實際上是coroutine_handle
的一個靜態(tài)函數(shù):
template <class _Promise> struct coroutine_handle { static coroutine_handle from_promise(_Promise& _Prom) noexcept { ... } ... }
這樣看來的話,我們只需要在get_return_object
函數(shù)調(diào)用時,先獲取coroutine_handle
,然后再傳給即將構(gòu)造出來的Generator
就行,因此我們稍微修改一下前面的代碼:
struct Generator { struct promise_type { ... // 構(gòu)造協(xié)程的返回值類型 Generator get_return_object() { return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) }; } ... }; std::coroutine_handle<promise_type> handle; int next() { handle.resume(); return ???; } };
接下來就是如何獲取協(xié)程內(nèi)部傳出來的值的問題了。同樣,本著有事找promise_type
的原則,我們可以直接給它定義一個value
成員:
struct Generator { struct promise_type { int value; ... }; std::coroutine_handle<promise_type> handle; int next() { handle.resume(); // 通過 handle 獲取 promise,然后再取到 value return handle.promise().value; } };
協(xié)程內(nèi)部掛起并傳值
現(xiàn)在的問題就是如何從協(xié)程內(nèi)部傳值給promise_type
了。
我們再觀察一下最終實現(xiàn)的效果:
Generator sequence() { int i = 0; while (true) { co_await i++; } }
特別需要注意的是co_await i++;
這一句,我們發(fā)現(xiàn)co_await
后面的是一個整型值,而不是我們在前面文章中提到的滿足等待體(awaiter)
條件的類型,這種情況下該怎么辦呢?
實際上,對于co_await <expr>
表達式中expr
的處理,C++有一套完善的流程:
- 如果
promise_type
當(dāng)中定義了await_transform
函數(shù),那么先通過promise.await_transform(expr)
來對expr
做一次轉(zhuǎn)換,得到的對象則為awaitable
;否則awaitable
就是expr
本身。 - 接下來使用
awaitable
對象獲取等待體(awaiter)
。如果awaitable
對象有operator co_await
運算符重載,那么等待體就是operator co_await(awaitable)
,否則等待體就是awaitable
對象本身。
按照上面的說法,我們要么給promise_type
實現(xiàn)一個await_transform(int)
函數(shù),要么就為整型實現(xiàn)一個operator co_await
的運算符重載,二者選一個就可以了。
方案 1:實現(xiàn) operator co_await
這個方案就是給 int 定義 operator co_await 的重載:
auto operator co_await(int value) { struct IntAwaiter { int value; bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle<Generator::promise_type> handle) const { handle.promise().value = value; } void await_resume() { } }; return IntAwaiter{.value = value}; }
當(dāng)然,這個方案對于我們這個特定的場景下是行不通的,因為在 C++ 當(dāng)中我們是無法給基本類型定義運算符重載的。
不過,如果我們遇到的情況不是基本類型,那么運算符重載的思路就可以行得通。operator co_await
的重載我們將會在后面給出例子。
方案 2:await_transform
運算符重載行不通,那就只能通過 await_tranform 來做轉(zhuǎn)換了。
代碼比較簡單:
struct Generator { struct promise_type { int value; // 傳值的同時要掛起,值存入 value 當(dāng)中 std::suspend_always await_transform(int value) { this->value = value; return {}; } ... }; std::coroutine_handle<promise_type> handle; int next() { handle.resume(); // 外部調(diào)用者或者恢復(fù)者可以通過讀取 value return handle.promise().value; } };
定義了 await_transform
函數(shù)之后,co_await expr
就相當(dāng)于 co_await promise.await_transform(expr)
了。
至此,我們的例子就可以運行了:
Generator sequence() { int i = 0; while (true) { co_await i++; } } int main() { auto gen = sequence(); for (int i = 0; i < 5; ++i) { std::cout << gen.next() << std::endl; } }
運行結(jié)果如下:
0
1
2
3
4
協(xié)程的銷毀
雖然我們的協(xié)程已經(jīng)能夠正常工作,但它仍然存在一些問題。
問題1: 無法確定是否存在下一個元素
當(dāng)外部調(diào)用者或者恢復(fù)者試圖調(diào)用 next
來獲取下一個元素的時候,它其實并不知道能不能真的得到一個結(jié)果。程序也可能拋出異常:
如下例:
Generator sequence() { int i = 0; // 只傳出 5 個值 while (i < 5) { co_await i++; } } int main() { auto gen = sequence(); for (int i = 0; i < 15; ++i) { // 試圖讀取 15 個值 std::cout << gen.next() << std::endl; } return 0; }
程序的結(jié)果是什么呢?
0
1
2
3
4
4Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
最后一個輸出的 4 實際上是恰好遇到協(xié)程銷毀之前的狀態(tài),此時 promise 當(dāng)中的 value 值還是之前的 4。而當(dāng)我們試圖不斷的去讀取協(xié)程的值,程序就拋出 SIGSEGV 的錯誤。錯誤的原因你可能已經(jīng)想到了,當(dāng)協(xié)程體執(zhí)行完之后,協(xié)程的狀態(tài)就會被銷毀,如果我們再訪問協(xié)程的話,就相當(dāng)于訪問了一個野指針。
為了解決這個問題,我們需要增加一個 has_next 函數(shù),用來判斷是否還有新的值傳出來,has_next 函數(shù)調(diào)用的時候有兩種情況:
- 已經(jīng)有一個值傳出來了,還沒有被外部消費
- 還沒有現(xiàn)成的值可以用,需要嘗試恢復(fù)執(zhí)行協(xié)程來看看還有沒有下一個值傳出來
這里我們需要有一種有效的辦法來判斷 value 是不是有效的,單憑 value 本身我們其實是無法確定它的值是不是被消費了,因此我們需要加一個值來存儲這個狀態(tài):
struct Generator { // 協(xié)程執(zhí)行完成之后,外部讀取值時拋出的異常 class ExhaustedException: std::exception { }; struct promise_type { int value; bool is_ready = false; ... } ... }
我們定義一個成員 state 來記錄協(xié)程執(zhí)行的狀態(tài),狀態(tài)的類型一共三種,只有 READY 的時候我們才能拿到值。
接下來改造 next
函數(shù),同時增加 has_next
函數(shù)來描述協(xié)程是否仍然可以有值傳出:
struct Generator { ... bool has_next() { // 協(xié)程已經(jīng)執(zhí)行完成 if (handle.done()) { return false; } // 協(xié)程還沒有執(zhí)行完成,并且下一個值還沒有準備好 if (!handle.promise().is_ready) { handle.resume(); } if (handle.done()) { // 恢復(fù)執(zhí)行之后協(xié)程執(zhí)行完,這時候必然沒有通過 co_await 傳出值來 return false; } else { return true; } } int next() { if (has_next()) { // 此時一定有值,is_ready 為 true // 消費當(dāng)前的值,重置 is_ready 為 false handle.promise().is_ready = false; return handle.promise().value; } throw ExhaustedException(); } };
注意,我們在promise_type
中產(chǎn)生新值后需要將is_ready
置為true
如下:
struct Generator { ... struct promise_type { ... std::suspend_always await_transform(T value) { this->value = value; is_ready = true; return {}; } }; };
這樣外部使用時就需要先通過 has_next 來判斷是否有下一個值,然后再去讀取了:
... int main() { auto generator = sequence(); for (int i = 0; i < 15; ++i) { if (generator.has_next()) { std::cout << generator.next() << std::endl; } else { break; } } return 0; }
問題2:協(xié)程狀態(tài)的銷毀比Generator對象的銷毀更早
我們前面提到過,協(xié)程的狀態(tài)在協(xié)程體執(zhí)行完之后就會銷毀,除非協(xié)程掛起在 final_suspend
調(diào)用時。
我們的例子當(dāng)中 final_suspend
返回了 std::suspend_never
,因此協(xié)程的銷毀時機其實比 Generator 更早:
auto generator = sequence(); for (int i = 0; i < 15; ++i) { if (generator.has_next()) { std::cout << generator.next() << std::endl; } else { // 協(xié)程已經(jīng)執(zhí)行完,協(xié)程的狀態(tài)已經(jīng)銷毀 break; } } // generator 對象在此仍然有效
這看上去似乎問題不大,因為我們在前面通過 has_next
的判斷保證了讀取值的安全性。
但實際上情況并非如此。我們在 has_next
當(dāng)中調(diào)用了 coroutine_handle::done
來判斷協(xié)程體是否執(zhí)行完成,判斷之前很可能協(xié)程已經(jīng)銷毀,coroutine_handle
這時候都已經(jīng)是無效的了:
bool has_next() { // 如果協(xié)程已經(jīng)執(zhí)行完成,理論上協(xié)程的狀態(tài)已經(jīng)銷毀,handle 指向的是一個無效的協(xié)程 // 如果 handle 本身已經(jīng)無效,因此 done 函數(shù)的調(diào)用此時也是無效的 if (handle.done()) { return false; } ... }
因此為了讓協(xié)程的狀態(tài)的生成周期與 Generator
一致,我們必須將協(xié)程的銷毀交給 Generator
來處理:
struct Generator { class ExhaustedException: std::exception { }; struct promise_type { ... // 總是掛起,讓 Generator 來銷毀 std::suspend_always final_suspend() noexcept { return {}; } ... }; ... ~Generator() { // 銷毀協(xié)程 handle.destroy(); } };
問題3:復(fù)制對象導(dǎo)致協(xié)程被銷毀
這個問題確切地說是問題 2的解決方案不完善引起的。
我們在 Generator 的析構(gòu)函數(shù)當(dāng)中銷毀協(xié)程,這本身沒有什么問題,但如果我們把 Generator 對象做一下復(fù)制,例如從一個函數(shù)當(dāng)中返回,情況可能就會變得復(fù)雜。例如:
Generator returns_generator() { auto g = sequence(); if (g.has_next()) { std::cout << g.next() << std::endl; } return g; } int main() { auto generator = returns_generator(); for (int i = 0; i < 15; ++i) { if (generator.has_next()) { std::cout << generator.next() << std::endl; } else { break; } } return 0; }
這段代碼乍一看似乎沒什么問題,但由于我們把 g
當(dāng)做返回值返回了,這時候 g
這個對象就發(fā)生了一次復(fù)制,然后臨時對象被銷毀。接下來的事兒大家就很容易想到了,運行結(jié)果如下:
0
-572662307Process finished with exit code -1073741819 (0xC0000005)
為了解決這個問題,我們需要妥善地處理 Generator 的復(fù)制構(gòu)造器:
struct Generator { ... explicit Generator(std::coroutine_handle<promise_type> handle) noexcept : handle(handle) {} Generator(Generator &&generator) noexcept : handle(std::exchange(generator.handle, {})) {} Generator(Generator &) = delete; Generator &operator=(Generator &) = delete; ~Generator() { if (handle) handle.destroy(); } }
我們只提供了右值復(fù)制構(gòu)造器,對于左值復(fù)制構(gòu)造器,我們直接刪除掉以禁止使用。原因也很簡單,對于每一個協(xié)程實例,都有且僅能有一個 Generator 實例與之對應(yīng),因此我們只支持移動對象,而不支持復(fù)制對象。
使用co_yield
序列生成器這個需求的實現(xiàn)其實有個更好的選擇,那就是使用 co_yield
。co_yield
就是專門為向外傳值來設(shè)計的,如果大家對其他語言的協(xié)程有了解,也一定見到過各種 yield
的實現(xiàn)。
C++ 當(dāng)中的 co_yield expr
等價于 co_await promise.yield_value(expr)
,我們只需要將前面例子當(dāng)中的 await_transform
函數(shù)替換成 yield_value
就可以使用 co_yield
來傳值了:
struct Generator { class ExhaustedException: std::exception { }; struct promise_type { ... // 將 await_transform 替換為 yield_value std::suspend_always yield_value(int value) { this->value = value; is_ready = true; return {}; } ... }; ... }; Generator sequence() { int i = 0; while (i < 5) { // 使用 co_yield 來替換 co_await co_yield i++; } }
可以看到改動點非常少,運行效果與前面的例子一致。
盡管可以實現(xiàn)相同的效果,但通常情況下我們使用 co_await
更多的關(guān)注點在掛起自己,等待別人上,而使用 co_yield
則是掛起自己傳值出去。因此我們應(yīng)該針對合適的場景做出合適的選擇。
使用序列生成器生成fibonacci數(shù)列
接下來我們要使用序列生成器來實現(xiàn)一個更有意義的例子,即斐波那契數(shù)列。
Generator fibonacci() { co_yield 0; // fib(0) co_yield 1; // fib(1) int a = 0; int b = 1; while(true) { co_yield a + b; // fib(N), N > 1 b = a + b; a = b - a; } }
我們看到這個實現(xiàn)非常的直接,完全不需要考慮 fib(N - 1) 和 fib(N - 2) 的存儲問題。
如果沒有協(xié)程,我們的實現(xiàn)可能是這樣的:
class Fibonacci { public: int next() { // 初值不符合整體的規(guī)律,需要單獨處理 if (a == -1){ a = 0; b = 1; return 0; } int next = b; b = a + b; a = b - a; return next; } private: int a = -1; int b = 0; };
使用時先構(gòu)造一個 Fibonacci 對象,然后調(diào)用 next 函數(shù)來獲取下一個值。對比之下,協(xié)程的實現(xiàn)帶來的好處是顯而易見的。協(xié)程生成的這個序列是個懶序列(Lazy Sequence),沒用到的項就不會被生成。
總結(jié)
本文圍繞序列生成器這個經(jīng)典的協(xié)程案例介紹了協(xié)程的銷毀、co_await 運算符、await_transform 以及 yield_value 的用法。
以上就是C++協(xié)程實現(xiàn)序列生成器的案例分享的詳細內(nèi)容,更多關(guān)于C++序列生成器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Qt實現(xiàn)將qsqlite數(shù)據(jù)庫中的數(shù)據(jù)導(dǎo)出為Excel表格
這篇文章主要為大家詳細介紹了如何通過Qt實現(xiàn)將qsqlite數(shù)據(jù)庫中的數(shù)據(jù)導(dǎo)出為Excel表格,文中的示例代碼簡潔易懂,有需要的小伙伴可以了解一下2024-12-12C++中成員函數(shù)和友元函數(shù)的使用及區(qū)別詳解
大家好,本篇文章主要講的是C++中成員函數(shù)和友元函數(shù)的使用及區(qū)別詳解,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下2022-01-01