Demystifying C++20 Coroutines


許久未在這兒寫文章了,從公眾號搬幾篇原創過來,感興趣的可以去關注一波。

0. 前言(Introduction)

這篇文章構思了許久。

初時不知從何寫起,協程的背后是整個並發,所涉知識極多,對於標准C++來說,也算是一個新概念。

思忖良久,欲以幾篇而述之,便先起手了此「概念篇」。

了解C++的會發現自C++11開始,很多更新都集中在並發支持上。

從最初的線程基礎支持,到如今的協程,C++已經日趨完善。

協程便是線程之后的又一利器,若論年齡,協程倒比線程要大。由於早期的線程主要在單CPU上運行,僅是模擬多線程,故又稱“偽線程”。偽線程和協程的思路頗有相似之處,以致難以旌別。

其實線程和協程之間不分軒輊,各有其應用場景。具體辨別,見本文第1,2節。

可能有人只想知其用法,不願細琢概念。但所謂“勿在浮沙築高樓”,概念不清,必會偏見多疑,難為准的。唯有纖悉洞了,小大靡遺,開擴來學,臻乎無惑,尚能步步銜進,獨覽眾山。

本篇將回答下列問題:

  • 什么是協程?
  • 協程與進程和線程有何區別?
  • 什么是並發,和並行有何區別
  • 協程和函數有何區別?
  • 什么是有棧協程,無棧協程?
  • 協程的使用場景有哪些?
  • Coroutines TS是什么?

本篇旨在明析概念,所以不會出現太多代碼。概念清晰后,下篇將有大量代碼來解析協程。

1. 並發與並行(Concurrency versus Parallelism)

要理清協程的定位,須得先能夠旌別並發與並行。

可以先來看看百科的定義:

並行是指“並排行走”或“同時實行或實施”。

在操作系統中是指,一組程序按獨立異步的速度執行,無論從微觀還是宏觀,程序都是一起執行的。

對比地,並發是指:在同一個時間段內,兩個或多個程序執行,有時間上的重疊(宏觀上是同時,微觀上仍是順序執行)。

這里用了兩個詞:宏觀和微觀。宏觀指從大的角度來看,微觀指從小的角度來說。

通俗地說,宏觀指的是能夠通過眼睛看到的,微觀則是眼睛看不到的。

並發在宏觀上同時,微觀上為順序執行。這就是偽線程的實現思路,通過迅速地在各個模塊之間切換執行,以迷惑視覺,使得看起來像是同時執行的,其實底層依舊是順序執行的。

線程在早期並非必須,因為都是命令窗口,就是從上往下依次執行的。而Windows擁有界面,在執行任務之時可以隨意進行別的操作,因此界面常常會卡死。

ms便搞了個模擬同步運行的機制,便是線程。了解過CPU發展歷史的會知道,早期CPU頻率步步直升,人們都以為這種趨勢能夠持續下去。這意味着你寫的程序不用更新也能隨着CPU的增強而自動增強性能。

所以早期都是單核CPU。隨着物理瓶頸到來,頻率提升之路越來越艱難,生產廠商便轉變策略,通過將多個核心置於一起,產生多核CPU來提升性能。這便是性能不夠,數量來湊。

也因此,真正的多線程得以實現。

真線程的執行便為並行執行。由於每個核心都是獨立的,因此可以同時執行。此時,無論是視覺上,還是底層實現上,都是真正的同時執行。

既然有並發與並行兩種形式,那么我們對其進行排列組合,便能得出4種結果。

一個既無並行,也無並發的程序處於並發的能力最弱,只能順序執行每條任務,這一般是一個很小的程序。

當有並發,無並行時,能夠充分發揮單核CPU的性能,偽線程與協程便屬此列。

而無並發,有並行時,也就是說是多核CPU,但一個核心上只開了一個線程,此時的確是並發。但和有並發無並行時所達到的效果是一樣的,真線程便屬此列。

也正因如此,線程和協程的效果只用眼睛是無法分辨的。有很多人便說有了多線程了為啥還要加入協程呢?其實線程和協程分工並不同,一個是並發,一個是並行,不過偽線程也屬於並發罷了。

那現在你可能又要改變問題了,既然偽線程也是並行,那么請問協程和偽線程又如何區分?

這個問題才是關鍵,起初聽說協程時,我便疑惑既然和偽線程一樣,為啥還要協程呢?其實主要區別是調度問題,本文第3節將詳論。

只有並發,或只有並行,都無法處理高並發需求,所以二者呈互補之勢,共同發揮CPU的最大性能。

下面再以一個例子來談談並發與並行。

假設有一個迷宮,

現欲尋得迷宮出口,便有四種方式。

在無並發無並行的情況下,只能暴力式的遍歷每條路徑。因此,在發現一條路不通時,需要原路返回到分岔路口,以接着嘗試另一條路徑。這意味着很多條路徑都要走兩次,效率可想而知。

在無並發有並行的情況下,便意味着有多人在同時尋找出口,即使其中一個人走的是死角,其他人亦可繼續自己的任務。

如此一來,每條路便只需走一遍。當然,這也意味着所耗費的資源也更多,此處對應的便是人力,在計算機中,對應的便是CPU核心數。

核心數無法持續增加,所以此處便有些是理想狀態了,實際情況可能得一人負責多條路徑。也就是說一個核上分成多個線程,此時便是真偽線程混合,並發與並行混合了。

那么只有並發是什么情況呢?

這時的情況是這樣的:一個人先走一部分,接着其他人再走,因為是並行執行,所以這些人無法同時行走。其中一個人走一會兒,停下來,其他人才能接着走。

這樣的好處是什么呢?

好處是即使一個人走入死胡同,也無需再原路返回。

什么意思呢?簡單地說,當每個人停止行走時,都需要轉到其他人那里去執行。他怎么瞬間跑到其他人那里去呢?其實每個人在跳轉之前,都會在原地插一個眼做標記,當需要跳轉時,便可直接傳送過去。

所以在一個人步入死角時,便會放棄這條路,傳送到其他人那里繼續執行。最后留下的一個人,便是正確的迷宮出口。

和並行不同,並發所需的資源要少,可以開啟上億個協程行動,他們之間互相協作,無論多大的迷宮,也能很快找到出口。

最后,並行與並發結合,無疑會指數級提升性能,因為並發說到底同時還是只有一個人能行動,而並發能同時多人行動,兩者互補,才能發揮出最大效率。

2. 量子計算(Quantum computation)

上述所說的統一稱為「經典計算」。既然談到了並行,便不得不再說下「量子計算」了。

通過上面介紹,可知「經典計算」要增加計算能力,先是嘗試提高處理器頻率,無奈達到了物理瓶頸。於是便增加物理資源,用數量來湊。

然而,這樣的方法來增加計算能力同樣是不可持續的,而且增加核心要消耗更多的物理資源。

第一個分岔路口需要2個人,第二個便要4個人,接着便要8個,16個,32個……

這個數字是呈指數增長的,要不了多久,便成了天文數字。

這也是現如今許多密碼學加密技術的依賴,通常數字都會非常大,計算機跑幾十年可能都跑不出來。這就是經典計算的瓶頸,理論上不可達。

經典計算的算力只能線性增加,而問題規模卻是指數增長的,所以經典計算根本追不上。

而量子計算則不同,隨着量子比特的增大,其信息容量也會呈線性增長。

像上面的迷宮,若是由量子計算來遍歷會如何做呢?

比如其有10個人,那么便能同時分出1024個分身,這樣所有的路徑都能被搜索到,只需走一次,就能找出迷宮出口。這被稱為量子疊加態的並行演化。

同時,量子還有另一個特性——糾纏性,可以解決我們在開發並行程序時需要格外觀注的數據競爭問題,通常都需要通過鎖或原子量來同步數據。

而量子糾纏天生就帶有同步性,一個數據被改變了,不論別處還有多少個數據,瞬間全部都會改變。比如有一個分身死了,那么1024個分身便全死了。

所以面對暴力計算,量子計算都只表示笑笑不說話。不過量子計算還在繼續發展,現在,我們只能使用經典計算來寫並發程序。

3. 協程(Coroutines)

注:本節介紹的協程不具體指某一語言中的,示例亦為偽碼,C++20的協程將於第6節介紹。

弄清楚了並發概念,便可以開始講協程了,先來看看wiki上的定義:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.

簡言之,協程,用於實現協作式多任務,屬於同一個進程的多個協程,在同一時刻只有一個處於運行狀態。

協程屬於並發形式的一種創建方式。

協程由兩部分組成的:

可以說,函數(Functions)之間擁有了協作(Cooperative)的能力,便稱之為協程(Coroutines)。

比如,現有一個普通的函數:

auto happy()
{
    return ": )";  // 在此處傳值並退出函數
}

auto emoji = happy();  // 在此處調用函數

此函數返回一個字符串,可以直接進行輸出。當調用函數時,只能等到函數執行結束才算結束調用。

現在對表情進行擴展,

void emoji()
{
    vector<string> emoji { ": )", ": (", "^-^", "@_@", "=^=" };
    for(auto p = emoji.begin(); p != emoji.end(); ++p)
    {
        cout << *p << " ";
    }
    cout << endl;
}

現在,我們想在不改變該函數的情況下進入如下輸出:

1. : )
2. : (
3. ^-^
4. @_@
5. =^=

於是需要引出另一個函數:

void add_lines()
{
    for(int i = 1;; ++i)
    {
        cout << i << ": ";
        cout << endl;
    }
}

可是,即使如此,依舊無法得到想要的輸出。此時,只有這兩個函數互相協作才能完成任務。

注意這兩個函數的執行順序,他們是交叉執行的,而非像普通函數那樣調用后執行完直接返回。

這里有幾個關鍵點,

第一,被調用函數(emoji)需要知道調用函數(add_lines)的返回地址,這樣才能在輸出后再跳回到調用方繼續執行。

第二,調用方和被調用方需要記住先前局部變量的值,用於在下一次執行時恢復現場。

第三,被調用方可以主動讓出控制流,以讓調用方恢復執行。

實際上,協程本質上就是對控制流的主動讓出和恢復機制,這也是其與函數之間的區別。

協程是函數的增強版,函數是協程的弱化版。

函數只能啟動(start)和終止(finish)執行,而協程在此基礎上,增加了掛起(suspend)和恢復(resume)能力。

那么如何完成上述流程呢?

可以來增加掛起和恢復機制:

// 偽碼表示,僅作說明之用

struct coro_frame {
    using iter_type = vector::iter;
    iter_type iter;
    int index;
    
    resume() {
        switch(index) {
        case 0:
            goto flag_r0;  // 從flag_r0恢復執行
        default:
            goto flag_default;    
        }
    }
};

coro_frame* emoji()
{
    void* handle = CORO_BEGIN(malloc);  // 分配狀態所需的空間
    coro_frame* frame = (coro_frame*)handle;  // 轉換為frame
    
    vector<string> emoji { ": )", ": (", "^-^", "@_@", "=^=" };
    for(frame->iter = emoji.begin(); frame->iter != emoji.end(); ++frame->iter)
    {
        frame.index = 0;  // 記錄暫停點
        CORO_SUSPEND(handle);  // 暫停執行,掛起協程
        RETURN_OBJECT(frame);  // 返回控制句柄,以便從外部恢復
    flag_r0:
        print(frame->iter);
    }
    println();
    CORO_END(hdl, free);  // 協程結束,銷毀申請的空間
}

void add_lines()
{
    auto frame = await emoji();
    for(int i = 1;; ++i)
    {
        println(i);
        frame.resume();
        println();
    }
}

可以看到我們對原有代碼添加了許多額外代碼來滿足掛起恢復的能力,實際上編譯器在生成協程時也會有這樣的操作,不過那是一個非常完善的機制了。

相信通過上述內容,大家已經對協程有了大致的印象,那么本節的任務也就完成了。

3.1 有棧協程(Stackful Coroutines) and 無棧協程(Stackless Coroutine)

函數在調用之前會將所需的參數壓入棧中以供使用,局部的數據也都保留在棧中,這一系列所需的數據稱為狀態(state),需要在調用方和被調用方之間來保存這些狀態,才能完成函數的調用和返回動作。

協程也需要保存一些狀態,而且因為要支持掛起和恢復功能,所以要保存的狀態也要比函數多。協程需要保存如下狀態:

  • Promise對象
  • 各個形參
  • 當前掛起點的位置,以便恢復時跳轉
  • 局部變量和臨時量

此時,便會有不同方式來保存這些狀態。主流的兩種,一種是有棧協程,另一種是無棧協程。

有棧協程,如其名,將數據保存在棧中;而無棧協程,會將主要數據采用堆來動態分配,只有少量狀態存放在棧上,此時只能掛起處於停層的函數。

有棧協程的生命期和它們的棧一樣長,無棧協程的生命期和它們的對象一樣長。

有棧協程的數據在被調用方分配,而無棧協程的數據在調用方分配。

關於此二者的區別,已經有文章詳細地描述了,所以我便不再重復寫了。大家可以參考這篇文章:https://blog.panicsoftware.com/coroutines-introduction/

其實現在了解與否並無關緊要,只需記往C++20的協程是無棧協程便好了。

4. 進程、線程和協程的區別(Distinguish between process, thread and coroutine)

關於進程線程,過去已經寫過一些文章了,我便不再詳細介紹。

關鍵是需要弄清楚並發和並行的區別,這也在文章開始詳細介紹了。

現在,只需下表的對比,就能清晰地了解這三者之間的關系與差別。

進程 線程 協程
切換者 操作系統 操作系統 用戶(編程者/應用程序)
切換時機 根據操作系統自己的切換策略,用戶不感知 根據操作系統自己的切換策略,用戶不感知 用戶自己(的程序)決定
切換內容 頁全局目錄
內存棧
硬件上下文
內核棧
硬件上下文
硬件上下文
切換內容的保存 保存於內核棧中 保存於內核棧中 保存於用戶自己的變量(用戶棧或者堆)
切換過程 用戶態-內核態-用戶態 用戶態-內核態-用戶態 用戶態(沒有陷入內核態)
切換效率

正是因為進程和線程的上下文切換效率低,所以才加入了協程來提高並發的能力。

5. C++20 Coroutines TS

5.1 初識C++協程

由前所述,我們知道協程是一個分步執行,遇到條件會掛起,直到滿足條件才會被喚醒以繼續執行后面代碼的並行方式。

C++准備了許久,終於在今年加入了協程。

不過C++20的協程標准仍處於「原理層」,只包含了編譯器需要實現的底層功能,是專門給庫開發者使用的,並沒有提供屬於「應用層」的高級庫給普通程序員使用。

這意味着要想使用協程,要么自己從原理層學起,自己封裝所需的功能,要么等待C++23的標准協程庫或第三方庫。

我們自然選擇前者,因此要學習的東西就比較多了,本篇就是先帶大家入協程的門,后面再層層深入。

C++20提供了三個新的關鍵字來支持協程,

  • co_await
  • co_yield
  • co_return

co_await可以掛起和恢復函數的執行,是主要需要學習的一個關鍵字。

co_yield可以在不結束協程的情況下從協程返回一些值。因此,可以用它來編寫無終止條件的生成器函數。

co_return允許從協程返回一些值,不過這需要我們稍加定制。

只要一個函數中包含了上面三個關鍵字中的任意一個,那么這個函數就是一個協程。

比如一個生成器函數:

generator<int> generate_numbers(int begin, int inc = 1)
{
    for(int i = begin;; i += inc)
        co_yield i;
}

因為generate_numbers函數中包含了co_yield關鍵字,所以它就是一個協程。

你也許已經看到,該協程沒有結束條件,因此它可以無限地產生值。

但就此一點代碼可還無法完成工作,稍后再來完善它,在此之前,還需了解幾個重要的組件。

5.2 Custom Coroutine Functor and Custom Promise

我們已經知道可通過co_yield來掛起協程,但是掛起之后又該如何恢復呢?

因此,需要有一種方式可以和協程進行溝通,當協程掛起的時候,可以通過這種方式來進行恢復。

這種方式便是Custom Coroutine Functor(定制協程函數/仿函數),前面所寫生成器的返回類型generator便是我們對於生成器所定義的Coroutine Functor。

注:前面使用了協程關鍵字的generate_numbers也叫協程函數,為避免和此處的Coroutine Functor翻譯混淆,本文中所有協程函數都指前者,Coroutine Functor均以英文出現。

現在,來定義它:

template <typename T> class generator
{
public:
    // 恢復協程
    bool resume();
};

在該Coroutine Functor中,聲明了用於恢復協程的resume函數。

現在,進行編譯程序,可以獲取如下錯誤:

編譯器告訴我們,promise_type不是generator的成員,因此,在Coroutine Functor中需要遵循一些規范。

我們需要在成員中加入promise_type,這個東西稱為Promise對象,需要定義以下接口:

  • get_return_object
  • initial_suspend
  • final_suspend
  • return_void
  • un_handled_exception

當使用協程函數

auto nums = generate_numbers(1);

時,首先會掛起當前執行點,此時便會調用initial_suspend。

接着,通過get_return_object將Coroutine Functor返回給調用方,因此在稍后才能進行恢復。

當遇到未處理的異常時,便會調用un_handled_exception,可以在此進行捕獲。

結束時,會調用用return_void,來到final_suspend,協程結束,Coroutine Functor析構。

由這些接口可知,協程通過Promise對象來提交結果或返回異常。

現在來實現這些接口,

template <typename T> class generator
{
public:
	struct promise_type {
		using coro_handle = std::experimental::coroutine_handle<promise_type>;
		auto get_return_object() {
			return coro_handle::from_promise(*this);
		}
		
		auto initial_suspend() { return std::experimental::suspend_always{}; }
		auto final_suspend() { return std::experimental::suspend_always{}; }

		auto return_void() {}

		void un_handled_exception() { std::terminate(); }
	};
    
    bool resume();
};

現在,接口俱以實現。我們又發現其中又有幾個新東西:

  • coroutine_handle
  • suspend_always

進行下一步之前,需要先了解其概念。

5.3 Coroutine handle

Promise對象從協程內部操縱,用於提交結果和異常。

而Coroutine handle(協程句柄)則從協程外部操縱,專門用於管理協程上下文,可以恢復或銷毀協程。

和Promise對象不同,Coroutine handle在標准文件已經定義,

template <> 
struct coroutine_handle<void>;   // no promise access
 
template <typename _PromiseT>
struct coroutine_handle : coroutine_handle<>;  // general form

可以看到,有兩個形式,一個void特化版和一個通用形式。

Coroutine handle主要包含了以下幾個重要成員:

  • static coroutine_handle from_address(void*)
  • void* address()
  • void operator()()
  • explicit operator bool()
  • void resume()
  • void destroy()
  • bool done()

Coroutine Functor的恢復操作其實就是通過Coroutine handle的resume函數進行恢復協程的,擁有了它,也就擁有了控制協程的能力。

既然Coroutine Functor這么重要,那么如何為我們的Coroutine Functor配備上呢?

首先,編譯器如何把Coroutine Functor給我們?

想要就得主動點,因此要在promise_type中自己先定義一個co_handle類型:

struct promise_type {
    using coro_handle = std::experimental::coroutine_handle<promise_type>;
    auto get_return_object() {
	    return coro_handle::from_promise(*this);
    }
...

由此,我們的就給promise_type配上了Coroutine handle,通過from_promise函數,可以從promise來創建Coroutine handle。也可直接使用return generator{};。

之后,調用get_return_object函數便能得到匹配的Coroutine handle。

前面說過,Promise對象是從協程內部操縱的,因此get_return_object函數其實是給編譯器調用的。

那么搞了半天,我們要怎么用呢?

當協程首次掛起時,編譯器便會使用operator new分配空間,保存當前作用域內的各種信息,以在恢復時使用。

之后便會調用get_return_object(),並將此結果返回給調用方,也就是Coroutine Functor。

因此,我們需要在那時進行保存。

template <typename T> class generator
{
public:
	struct promise_type {
		...
	};
	using coro_handle = std::experimental::coroutine_handle<promise_type>;
	generator(coro_handle handle) : handle_(handle) { assert(handle_); }
	generator(const generator&) = delete;
	generator(generator&& other) : handle_(other.handle_) { other.handle_ = nullptr; };
	~generator() { handle_.destroy(); }
	
	bool resume() {
		if (!handle_.done())
			handle_.resume();
		return !handle_.done();
	}
	
private:
	coro_handle handle_;
};

可以看到,編譯器實際上調用了我們的構造函數,因此,必須提供Coroutine handle類型的構造函數。

之后,便可對協程進行操控。可以看到,經可以使用resume()函數來恢復協程了。

5.4 Awaitable object

Awaitable object叫作可等待的對象,或者應該說是擁有等待屬性的對象。

何時掛起,何時恢復,一定根據一個條件來決定。比如,當服務器接收數據時,在等待數據的時候便掛起,去執行別的邏輯。待數據到來,便恢復接收數據的操作。

這個對於條件的抽象便是Awaitable object,需要滿足三個接口:

  • bool await_ready()
  • void await_suspend(coroutine_handle<>)
  • auto await_resume()

每次調用co_await時,便會調用await_ready()來查看是否已滿足條件,滿足則表示萬事俱備,則編譯器會調用await_resume()恢復協程;若條件尚未完成,則會調用await_suspend()掛起協程,將控制權返還給調用者。

標准中提供了兩個trival Awaitable object,

suspend_never
{
   bool await_ready() { return true; }
   void await_suspend(coroutine_handle<>) {}
   auto await_resume() {}
};

suspend_always
{
   bool await_ready() { return false; }
   void await_suspend(coroutine_handle<>) {}
   auto await_resume() {}
};

suspend_always的await_ready()總是返回false,suspend_never的await_ready()總是返回true。

通常情況這兩個已經可以滿足需求了,但也有很多情況需要自己編寫特定版本,這些放到下篇專門來介紹。

5.5 Coroutine traits

回到生成器的協程函數:

generator<int> generate_numbers(int begin, int inc = 1)
{
    for(int i = begin;; i += inc)
        co_yield i;
}

編譯器需要從返回類型generator 來確定Promise的類型,其所使用的就是Coroutine traits(協程特性),標准中自帶了一個std::coroutine_traits,便是其實現。

所以我們的協程函數的Promise類型將被推導為:

std::coroutine_traits<generator<int>, int, int>::promise_type

5.6 co_yield && co_await && co_return

到此為止,大家基本上已經了解這三個關鍵字的概念了。

接下來將繼續完成co_yield所完成的生成器並介紹co_return,co_await是協程的關鍵角色,放到下篇專門來論。

因為要使用co_yield,所以還需給Promise增加一個yield_value接口:

template <typename T> class generator
{
public:
	struct promise_type {
        ...
        T value_;
		auto yield_value(T value) {
			value_ = value;
			return std::experimental::suspend_always{};
		}
    };
};

yield_value用於保存數據到Promise中,取出操作也得自己定義:

template <typename T> class generator
{
public:
	struct promise_type {
        ...
		T value_;
        // 保存生成的值
		auto yield_value(T value) {
			value_ = value;
			return std::experimental::suspend_always{};
		}
	};
    ...
    // 獲取生成的值
	const T get_generated_value() {
		return handle_.promise().value_;
	}
private:
	coro_handle handle_;
};

與yield_value不同的是,get_generated_value是我們自定義的,名字可以隨意起。

現在,便可以使用我們的協程了:

int main()
{
	auto nums = generate_numbers(1);
	for (;;)
	{
		nums.resume();
		std::cout << nums.get_generated_value() << " ";
		
		if (nums.get_generated_value() > 20) break;
	}

	return 0;
}

此處,使用生成器從1開始,依次以遞增1的趨勢生成數字,現在的結束條件完全在調用方這邊。因此,所有流程,都可由調用方進行控制。

輸出如下:

最后,簡單地介紹下co_return。

co_return用於協程的返回,就像return一樣,不過是專門針對協程的。

例如:

std::future<int> a()
{
	co_return 42;
}

當協程沒有結束條件時,就像上述定義的生成器,則需要定義return_void()或return_value()函數,若無定義,當協程結束時會遇到未定義的行為(一般沒定義會直接編譯不過)。

如果通過co_return返回void,則會調用return_void();通過co_return返回一些值時,則會調用return_value()。這兩個函數不能同時定義。

本節到此便已結束,相信大家對C++的協程也有了初步的認識,后面便能再寫一些稍微有難度的文章來繼續分享了。

本節完整的協程代碼如下:

#include <iostream>
#include <cassert>

#include <experimental/coroutine>


template <typename T> class generator
{
public:
	struct promise_type {
		using coro_handle = std::experimental::coroutine_handle<promise_type>;
		auto get_return_object() {
			return coro_handle::from_promise(*this);
		}
		
		auto initial_suspend() { return std::experimental::suspend_always{}; }
		auto final_suspend() { return std::experimental::suspend_always{}; }

		auto return_void() {}

		void un_handled_exception() { std::terminate(); }

		T value_;
		auto yield_value(T value) {
			value_ = value;
			return std::experimental::suspend_always{};
		}
	};
	using coro_handle = std::experimental::coroutine_handle<promise_type>;
	generator(coro_handle handle) : handle_(handle) { assert(handle_); }
	generator(const generator&) = delete;
	generator(generator&& other) : handle_(other.handle_) { other.handle_ = nullptr; };
	~generator() { handle_.destroy(); }

	bool resume() {
		if (!handle_.done())
			handle_.resume();
		return !handle_.done();
	}

	const T get_generated_value() {
		return handle_.promise().value_;
	}
private:
	coro_handle handle_;
};

generator<int> generate_numbers(int begin, int inc = 1)
{
	for (int i = begin;; i += inc)
		co_yield i;
}

int main()
{
	auto nums = generate_numbers(1);
	for (;;)
	{
		nums.resume();
		std::cout << nums.get_generated_value() << " ";
		
		if (nums.get_generated_value() > 20) break;
	}

	return 0;
}

6. 使用場景(Coroutines use cases)

  • generators
  • 異步IO
  • lazy computations
  • 事件驅動程序
  • 協作式多任務
  • 無限列表
  • ...

7. 總結(Summary)

本篇介紹了C++20協程的大多數概念,理解之后便算是協程入門了。

關於文章開頭的那些問題,想必大家心中也已有了答案。

但這些介紹仍只是其冰山一角,C++的協程還有非常多的知識需要學習,待日后再慢慢道來。

8. References


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM