C++20 Coroutines 協程

01 協程(coroutines)

協程就是用戶態線程內可以被異步執行的函數。用來在用戶態下解決異步問題。

普通函數:每次調用只能從第一條語句開始執行直到某個出口結束。
協程函數:協程函數交出控制權後,可以再次從交出控制權的下一語句開始執行。

在協程的叫法出現以前,處理異步問題一般會使用操作系統提供的系統級API來解決異步問題。系統級的API都是大多采用回調函數方式實現。後來人們覺得使用異步回調形式的API比較麻煩,就開始提供異步調用庫或者語言級支持。並且起來個優雅的名字–協同程序

協程不是線程。協程包含在線程內。協程在用戶態,由用戶控制。協程的切換比線程切換效率高。
同一個線程在一個時間點最多隻能跑一個協程;在同一個線程中,協程的運行是穿行的。索引沒有數據爭用(data race),也不需要鎖。
原則上,應該儘量避免將同一個協程的主體,放到不同的線程中同時執行。因爲這樣有很大概率發生數據爭用(data race)。[其實這種情況下就是線程的數據爭用問題]。所以我們應該在線程中討論協程;而不是在進程中討論協程。

目前在C/C++中比較知名的協程(類協程)庫有:
  • Boost.Fiber
  • Boost.Coroutine
  • fiber
  • libco
  • libtask

協程根據實現方式不同,分爲有棧協程(stackful coroutine)無棧協程(stackless coroutine)
有棧協程可通過操作系統提供的系統調用實現;無棧協程需要語言標準和編譯器支持。

OS 有棧協程系統調用
Linux getcontext,setcontext,makecontext,swapcontext
Windows CreateFiber,ConvertFiberToThread,SwitchToFiber

微軟擬提的C++20標準中(目前是ts,即:<experimental/coroutine>)的協程屬於stackless coroutine。

協程函數特點:

1:首次調用協程函數,會從堆中分配一個協程上下文,調用方的返回地址、入口函數、交出控制權等信息保存在協程上下文中。

2:當協程中途交出控制權後,協程上下文不會被刪除(相當於函數退出之後,上下文環境還被保存,類比線程切換)。

3:當協程再次獲得控制權後,會自動從協程上下文中恢復調用環境,然後從上一次交出控制權的下一條語句繼續執行(加載目標協程環境,類比線程切換)。

4:協程函數返回(非中途交出控制權)後,協程上下文將被刪除。

5:若再次調用協程函數,視爲首次調用。

有棧協程(Stackful Coroutines)特點:

1:每個協程都有一個預先分配的調用棧(Call Stack)。

2:每個協程都屬於且僅屬於創建它的線程。

3:一個線程可以包含多個協程。

4:線程本身也可以是一個協程,成爲主協程(Primary Coroutine)。

5:協程必須主動交出控制權,否則同一線程的其它協程均無法獲得執行機會。

6:協程執行路徑上,任何被調用的函數均可在任何位置交出控制權。

7:如果允許協程把控制權交給同一線程的其它協程,則稱爲對稱協程(Symmetry Coroutines)。如果只允許協程把控制權交給主協程,主協程作爲調度器,負責分配執行權,則稱爲非對稱協程(Asymmetry Coroutines)。

8:屬於同一線程的多個協程之間沒有數據爭用(Data Race)問題。

9:無需修改語言標準和編譯器,利用系統調用即可實現。

無棧協程(Stackless Coroutines)特點:

1:每個協程的執行環境,僅需包含調用棧(Call Stack)的頂層棧幀(Top Call Stack Frame),而非整個調用棧,因而空間開銷極小。

2:協程執行路徑上,只有特定語句才能交出控制權。

3:無需協程調度器。

4:調用協程函數時,同一協程函數的不同部分,有可能在不同的線程環境中執行。因此需要處理好數據爭用(Data Race)問題。

5:需要語言標準和編譯器支持。

有棧協程和無棧協程對比,有棧協程的最大缺陷是保存調用棧的開銷太大;
無棧協程不但具有有棧協程的所有優點,而且空間開銷極低;唯一不足就是需要語言標準和編譯器支持。

怎麼識別C++20中的協程

如果在C++20的一個函數體內包含co_awaitco_yieldco_return中任何一個關鍵字,那麼這個函數就是一個coroutine。其中:

co_await:掛起當前的coroutine。
co_return:從當前coroutine返回一個結果。
co_yield:返回一個結果並且掛起當前的coroutine。

一個coroutine必定包含Promise和Awaitable組成。

協程通過Promise和Awaitable接口來規範實現。實現最簡單的協程需要用到其中的8個(5個Promise的函數和3個Awaitable的函數)。

如果要實現形如co_await xxxxx;的協程調用格式, xxxxx就必須實現Awaitableco_await是一個新的運算符。Awaitable主要有3個函數:

await_ready:返回Awaitable實例是否已經ready。協程開始會調用此函數,如果返回true,表示你想得到的結果已經得到了,協程不需要執行了。所以大部分情況這個函數的實現是要return false。

await_suspend:掛起awaitable。該函數會傳入一個coroutine_handle類型的參數。這是一個由編譯器生成的變量。在此函數中調用handle.resume(),就可以恢復協程。

await_resume:當協程重新運行時,會調用該函數。這個函數的返回值就是co_await運算符的返回值。

02 C++20 Coroutine Demo

啓用ts中的Coroutine功能,需要vs2019中做如下配置:

配置屬性==>常規==>C++語言標準,預覽-最新C++工作草案中的功能(std:c++latest)
配置屬性==>C/C++==>命令行,添加/await

C++20 Coroutine配置設置
C++20 添加/await,支持coroutine
並且在代碼中加入實驗庫#include <experimental/coroutine>

下面的demo代碼收集在:
https://github.com/5455945/cpp_demo/tree/master/C%2B%2B20

02.01 coroutine demo01

這是 https://github.com/franktea/temp/blob/master/uncategorized/coroutine.md 中實現的。在這裏對每個函數斷點後,調試,對理解協程流程很有幫助。再結合C++ 協程介紹[譯]中的圖形解釋,會很有幫助。

// https://github.com/franktea/temp/blob/master/uncategorized/co_vs_callback.cpp
// https://github.com/franktea/temp/blob/master/uncategorized/coroutine.md
#include <iostream>
#include <thread>
#include <experimental/coroutine>
#include <chrono>
#include <functional>

#include "co_vs_callback.h"

 // clang++ -std=c++2a -fcoroutines-ts -lstdc++ co_vs_callback.cpp

using call_back = std::function<void(int)>;
void Add100ByCallback(int init, call_back f) // 異步調用
{
	std::thread t([init, f]() {
		std::this_thread::sleep_for(std::chrono::seconds(5));
		std::cout << "Add100ByCallback: " << init << std::endl;
		f(init + 100);
		});
	t.detach();
}

struct Add100Awaitable
{
	Add100Awaitable(int init) :init_(init) {}
	bool await_ready() const { return false; }
	int await_resume() { return result_; }
	void await_suspend(std::experimental::coroutine_handle<> handle)
	{
		auto f = [handle, this](int value) mutable {
			result_ = value;
			handle.resume();
		};
		Add100ByCallback(init_, f); // 調用原來的異步調用
	}
	int init_;
	int result_;
};

struct Task
{
	struct promise_type {
		auto get_return_object() { return Task{}; }
		auto initial_suspend() { return std::experimental::suspend_never{}; }
		auto final_suspend() { return std::experimental::suspend_never{}; }
		void unhandled_exception() { std::terminate(); }
		void return_void() {}
	};
};

Task Add100ByCoroutine(int init, call_back f)
{
	int ret = co_await Add100Awaitable(init);
	//ret = co_await Add100Awaitable(ret);
	//ret = co_await Add100Awaitable(ret);
	f(ret);
}

void co_vs_callback()
{
	//Add100ByCallback(5, [](int value) { std::cout << "get result: " << value << "\n"; });
	Add100ByCoroutine(10, [](int value) { std::cout << "get result from coroutine1: " << value << "\n"; });
	//Add100ByCoroutine(20, [](int value) { std::cout << "get result from coroutine2: " << value << "\n"; });
	//Add100ByCoroutine(30, [](int value) { std::cout << "get result from coroutine3: " << value << "\n"; });
	//Add100ByCoroutine(40, [](int value) { std::cout << "get result from coroutine4: " << value << "\n"; });
	std::this_thread::sleep_for(std::chrono::seconds(50));
}

02.02 Coroutine demo02 co_await

這個demo來自黑山是不是山黑C++ coroutine

// https://www.cnblogs.com/heishanlaoy/p/11760368.html
#include "co_vs_await.h"
#include <iostream>
#include <experimental/coroutine>
using namespace std;

template<class T>
struct test {
    // inner types
    struct promise_type;
    using handle_type = std::experimental::coroutine_handle<promise_type>; //type alias

                                                                           // functions
    test(handle_type h) :handle(h) { cout << "# Created a Test object\n"; }
    test(const test& s) = delete;
    test& operator=(const test&) = delete;
    test(test&& s) :handle(s.handle) { s.handle = nullptr; }
    test& operator=(test&& s) { handle = s.handle; s.handle = nullptr; return *this; }
    ~test() { cout << "#Test gone\n"; if (handle) handle.destroy(); }

    T get()
    {
        cout << "# Got return value\n";
        if (!(this->handle.done()))
        {
            handle.resume();  //resume
            return handle.promise().value;
        }
    }

    struct promise_type
    {
        promise_type() { cout << "@ promise_type created\n"; }
        ~promise_type() { cout << "@ promise_type died\n"; }

        auto get_return_object() //get return object
        {
            cout << "@ get_return_object called\n";
            return test<T>{handle_type::from_promise(*this)};// pass handle to create "return object"
        }

        auto initial_suspend() // called before run coroutine body
        {
            cout << "@ initial_suspend is called\n";
            // return std::experimental::suspend_never{}; // dont suspend it
            return std::experimental::suspend_always{};
        }

        auto return_void() // called when just before final_suspend, conflict with return_value
        {
            cout << "@ return_void is called\n";
            return std::experimental::suspend_never{}; // dont suspend it
                                                       //return std::experimental::suspend_always{};
        }

        auto yield_value(int t) // called by co_yield()
        {
            std::cout << "yield_value called\n";
            value = t;
            return std::experimental::suspend_always{};
        }

        auto final_suspend() // called at the end of coroutine body
        {
            cout << "@ final_suspend is called\n";
            return std::experimental::suspend_always{};
        }

        void unhandled_exception() //exception handler
        {
            std::exit(1);
        }

        //T await_transform() {}

        // data
        T value;
    };

    // member variables
    handle_type handle;
};

struct AwaiableObj
{
    int a;
    AwaiableObj() :a(0) {}
    bool await_ready()
    {
        cout << "@@ await_ready called\n";
        return true;
    }

    auto await_suspend(std::experimental::coroutine_handle<> awaiting_handle)
    {
        cout << "@@ await_suspend called\n";
        // return ;
        // return true;
        return false;
        // return awaiting_handle;
    }

    auto await_resume()
    {
        cout << "@@ await_resume called\n";
        return a++;
    }
};

test<int> await_routine()
{
    auto a = AwaiableObj{};
    for (int i = 0; i < 5; i++)
    {
        auto v = co_await a;
        co_yield v;
    }
}

void co_vs_await()
{
    auto a = await_routine();
    auto b = a.get();
    cout << "value is " << b << endl;
    b = a.get();
    cout << "value is " << b << endl;
    b = a.get();
    cout << "value is " << b << endl;
    b = a.get();
    cout << "value is " << b << endl;
    b = a.get();
    cout << "value is " << b << endl;
}

02.03 Coroutine demo02 co_return

這個demo來自黑山是不是山黑C++ coroutine

// https://www.cnblogs.com/heishanlaoy/p/11760368.html
#include "co_vs_return.h"
#include <iostream>
#include <experimental/coroutine>
using namespace std;

template<class T>
struct test {
    // inner types
    struct promise_type;
    using handle_type = std::experimental::coroutine_handle<promise_type>; //type alias

    // functions
    test(handle_type h) :handle(h) { cout << "# Created a Test object\n"; }
    test(const test& s) = delete;
    test& operator=(const test&) = delete;
    test(test&& s) :handle(s.handle) { s.handle = nullptr; }
    test& operator=(test&& s) { handle = s.handle; s.handle = nullptr; return *this; }
    ~test() { cout << "#Test gone\n"; if (handle) handle.destroy(); }

    T get()
    {
        cout << "# Got return value\n";
        if (!(this->handle.done()))
        {
            handle.resume();  //resume
            return handle.promise().value;
        }
    }

    struct promise_type
    {
        promise_type() { cout << "@ promise_type created\n"; }
        ~promise_type() { cout << "@ promise_type died\n"; }

        auto get_return_object() //get return object
        {
            cout << "@ get_return_object called\n";
            return test<T>{handle_type::from_promise(*this)};// pass handle to create "return object"
        }

        auto initial_suspend() // called before run coroutine body
        {
            cout << "@ initial_suspend is called\n";
            // return std::experimental::suspend_never{}; // dont suspend it
            return std::experimental::suspend_always{};
        }

        auto return_value(T v) // called when there is co_return expression
        {
            cout << "@ return_value is called\n";
            value = v;
            return std::experimental::suspend_never{}; // dont suspend it
                                                       //return std::experimental::suspend_always{};
        }

        auto final_suspend() // called at the end of coroutine body
        {
            cout << "@ final_suspend is called\n";
            return std::experimental::suspend_always{};
        }

        void unhandled_exception() //exception handler
        {
            std::exit(1);
        }

        // data
        T value;
    };

    // member variables
    handle_type handle;
};
test<int> return_coroutine()
{
    std::cout << "start return_coroutine\n";
    co_return 1;
    co_return 2; // will never reach here
}

void co_vs_return()
{
    auto a = return_coroutine();
    cout << "created a corutine, try to get a value\n";
    int an = a.get();
    cout << "value is " << an << endl;
    an = a.get();
    cout << "value is " << an << endl;
}

02.04 Coroutine demo02 co_yield

這個demo來自黑山是不是山黑C++ coroutine

// https://www.cnblogs.com/heishanlaoy/p/11760368.html
#include "co_vs_yield.h"
#include <iostream>
#include <experimental/coroutine>
using namespace std;

struct test {
    // inner types
    struct promise_type;
    using handle_type = std::experimental::coroutine_handle<promise_type>; //type alias

                                                                           // functions
    test(handle_type h) :handle(h) { cout << "# Created a Test object\n"; }
    test(const test& s) = delete;
    test& operator=(const test&) = delete;
    test(test&& s) :handle(s.handle) { s.handle = nullptr; }
    test& operator=(test&& s) { handle = s.handle; s.handle = nullptr; return *this; }
    ~test() { cout << "#Test gone\n"; if (handle) handle.destroy(); }

    int current_value() {
        return handle.promise().value;
    }

    bool move_next()
    {
        handle.resume();
        return !handle.done();
    }

    struct promise_type
    {
        promise_type() { cout << "@ promise_type created\n"; }
        ~promise_type() { cout << "@ promise_type died\n"; }

        auto get_return_object() //get return object
        {
            cout << "@ get_return_object called\n";
            return test{ handle_type::from_promise(*this) };// pass handle to create "return object"
        }

        auto initial_suspend() // called before run coroutine body
        {
            cout << "@ initial_suspend is called\n";
            return std::experimental::suspend_never{}; // dont suspend it
            //return std::experimental::suspend_always{};
        }

        auto return_void() // called when just before final_suspend, conflict with return_value
        {
            cout << "@ return_void is called\n";
            return std::experimental::suspend_never{}; // dont suspend it
                                                       //return std::experimental::suspend_always{};
        }

        auto yield_value(int t) // called by co_yield()
        {
            std::cout << "yield_value called\n";
            value = t;
            return std::experimental::suspend_always{};
        }

        auto final_suspend() // called at the end of coroutine body
        {
            cout << "@ final_suspend is called\n";
            return std::experimental::suspend_always{};
        }

        void unhandled_exception() // exception handler
        {
            std::exit(1);
        }

        // data
        int value;
    };

    // member variables
    handle_type handle;
};
test yield_coroutine(int count)
{
    std::cout << "start yield_coroutine\n";
    for (int i = 0; i < count; i++)
        co_yield i * 2;
}

void co_vs_yield()
{
    auto a = yield_coroutine(4);
    cout << "created a corutine, try to get a value\n";
    do
    {
        cout << "get value " << a.current_value() << endl;
    } while (a.move_next());
}

02.05 librf庫

librf是一個基於C++ Coroutines提案 ‘Stackless Resumable Functions’編寫的非對稱stackless協程庫。
https://github.com/tearshark/librf

參考

1.C++ 協程介紹[譯]
2.C++協程
3.C++ coroutine
4.co_vs_callback
5.isocpp.org的experimental
6.萬字長文 | 漫談libco協程設計及實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章