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é)合上一篇文章當中對于協(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 {}; }
// 為了簡單,我們認為序列生成器當中不會拋出異常,這里不做任何處理
void unhandled_exception() { }
// 構(gòu)造協(xié)程的返回值類型
Generator get_return_object() {
return Generator{};
}
// 沒有返回值
void return_void() { }
};
int next() {
???.resume();
return ???;
}
};
代碼當中有兩處我們標注為 ???,表示暫時還不知道怎么處理。
如果我們想要在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當中定義了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};
}
當然,這個方案對于我們這個特定的場景下是行不通的,因為在 C++ 當中我們是無法給基本類型定義運算符重載的。
不過,如果我們遇到的情況不是基本類型,那么運算符重載的思路就可以行得通。operator co_await 的重載我們將會在后面給出例子。
方案 2:await_transform
運算符重載行不通,那就只能通過 await_tranform 來做轉(zhuǎn)換了。
代碼比較簡單:
struct Generator {
struct promise_type {
int value;
// 傳值的同時要掛起,值存入 value 當中
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 就相當于 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: 無法確定是否存在下一個元素
當外部調(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 當中的 value 值還是之前的 4。而當我們試圖不斷的去讀取協(xié)程的值,程序就拋出 SIGSEGV 的錯誤。錯誤的原因你可能已經(jīng)想到了,當協(xié)程體執(zhí)行完之后,協(xié)程的狀態(tài)就會被銷毀,如果我們再訪問協(xié)程的話,就相當于訪問了一個野指針。
為了解決這個問題,我們需要增加一個 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
// 消費當前的值,重置 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)用時。
我們的例子當中 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 當中調(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ù)制對象導致協(xié)程被銷毀
這個問題確切地說是問題 2的解決方案不完善引起的。
我們在 Generator 的析構(gòu)函數(shù)當中銷毀協(xié)程,這本身沒有什么問題,但如果我們把 Generator 對象做一下復(fù)制,例如從一個函數(shù)當中返回,情況可能就會變得復(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 當做返回值返回了,這時候 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++ 當中的 co_yield expr 等價于 co_await promise.yield_value(expr),我們只需要將前面例子當中的 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ù)導出為Excel表格
這篇文章主要為大家詳細介紹了如何通過Qt實現(xiàn)將qsqlite數(shù)據(jù)庫中的數(shù)據(jù)導出為Excel表格,文中的示例代碼簡潔易懂,有需要的小伙伴可以了解一下2024-12-12
C++中成員函數(shù)和友元函數(shù)的使用及區(qū)別詳解
大家好,本篇文章主要講的是C++中成員函數(shù)和友元函數(shù)的使用及區(qū)別詳解,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下2022-01-01

