C++20協程


C++20協程

簡介

​ C++20協程只是提供協程機制,而不是提供協程庫。C++20的協程是無棧協程,無棧協程是一個可以掛起/恢復的特殊函數,是函數調用的泛化,且只能被線程調用,本身並不搶占內核調度。

​ C++20 提供了三個新關鍵字(co_await、co_yield 和 co_return),如果一個函數中存在這三個關鍵字之一,那么它就是一個協程。

​ 協程相關的三個關鍵字:co_await、co_yield與co_return

  • co_yield some_value: 保存當前協程的執行狀態並掛起,返回some_value給調用者
  • co_await some_awaitable: 如果some_awaitable沒有ready,就保存當前協程的執行狀態並掛起
  • co_return some_value: 徹底結束當前協程,返回some_value給協程調用者

協程相關的對象

協程幀(coroutine frame)

當 caller 調用一個協程的時候會先創建一個協程幀,協程幀會構建 promise 對象,再通過 promise 對象產生 return object。

協程幀中主要有這些內容:

  • 協程參數

  • 局部變量

  • promise 對象

這些內容在協程恢復運行的時候需要用到,caller 通過協程幀的句柄 std::coroutine_handle 來訪問協程幀。

promise_type

promise_type 是 promise 對象的類型。promise_type 用於定義一類協程的行為,包括協程創建方式、協程初始化完成和結束時的行為、發生異常時的行為、如何生成 awaiter 的行為以及 co_return 的行為等等。promise 對象可以用於記錄/存儲一個協程實例的狀態。每個協程楨與每個 promise 對象以及每個協程實例是一一對應的。

promise_type的接口

promise_type接口 功能
initial_suspend() 控制協程初始化完成后是否掛起
final_suspend() 控制協程執行完后是否掛起
get_return_object() 返回給 caller 一個對象
unhandled_exception() 處理異常
return_void() 調用co_return;時或者協程執行完后被調用
return_value(T) 保存協程返回值。調用co_return xxx;的時候被調用,保存協程返回值
yield_value() 調用co_yield xxx;的時候,會調用,保存協程返回值
await_transform() 用於定制協程body中co_await xxx;語句的行為。定義該方法后,編譯器會將出現在協程主體中的每個co_await xxx;轉換為co_await promise.await_transform(xxx)

promise_type里的接口需要我們實現,promise_type里的接口是給編譯器調用的

coroutine return object

它是promise.get_return_object()方法創建的,一種常見的實現手法會將 coroutine_handle 存儲到 coroutine object 內,使得該 return object 獲得訪問協程的能力。

std::coroutine_handle

協程幀的句柄,主要用於訪問底層的協程幀、恢復協程和釋放協程幀。
程序員可通過調用 std::coroutine_handle::resume() 喚醒協程。

std::coroutine_handle接口

coroutine_handle接口 作用
from_promise() 從promise對象創建一個coroutine_handle
done() 檢查協程是否運行完畢
operator bool 檢查當前句柄是否是一個coroutie
operator() 恢復協程的執行
resume 恢復協程的執行(同上)
destroy 銷毀協程
promise 獲取協程的promise對象
address 返回coroutine_handle的指針
from_address 從指針導入一個coroutine_handle

coroutine_handle的接口不需要我們實現,可以直接調用

co_await、awaiter、awaitable

  • co_await:一元操作符;

  • awaitable:支持 co_await 操作符的類型;

  • awaiter:定義了 await_ready、await_suspend 和 await_resume 方法的類型。

co_await expr(expr是表達式) 通常用於表示等待一個任務(可能是 lazy 的,也可能不是)完成。co_await expr 時,expr 的類型需要是一個 awaitable,而該 co_await表達式的具體語義取決於根據該 awaitable 生成的 awaiter。

協程對象如何協作

以一個簡單的代碼展示這些協程對象如何協作:

Return_t foo () { 
    auto res = co_await awaiter; 
    co_return res ; 
}

Return_t:promise return object。

awaiter: 等待一個task完成。

圖中淺藍色部分的方法就是 Return_t 關聯的 promise 對象的函數,淺紅色部分就是 co_await 等待的 awaiter。

這個流程的驅動是由編譯器根據協程函數生成的代碼驅動的,分成三部分:

  • 協程創建;
  • co_await awaiter 等待 task 完成;
  • 獲取協程返回值和釋放協程幀。

協程的創建

Return_t foo () { 
    auto res = co_await awaiter; 
    co_return res ; 
}

foo()協程會生成下面這樣的模板代碼(偽代碼),協程的創建都會產生類似的代碼:

{
	co_await promise.initial_suspend();
  	try
  	{
    	coroutine body;
  	}
  	catch (...)
  	{
    	promise.unhandled_exception();
  	}
FinalSuspend:
  	co_await promise.final_suspend();
}

首先需要創建協程,創建協程之后是否掛起則由調用者設置 initial_suspend 的返回類型來確定。

創建協程的流程大概如下:

  • 創建一個協程幀(coroutine frame)
  • 在協程幀里構建 promise 對象
  • 把協程的參數拷貝到協程幀里
  • 調用 promise.get_return_object() 返回給 caller 一個對象,即代碼中的 Return_t 對象

在這個模板框架里有一些可定制點:如 initial_suspend、final_suspend、unhandled_exception 和 return_value。

我們可以通過 promise 的 initial_suspend 和 final_suspend 返回類型來控制協程是否掛起,在 unhandled_exception 里處理異常,在 return_value 里保存協程返回值。

可以根據需要定制 initial_suspend 和 final_suspend 的返回對象來決定是否需要掛起協程。如果掛起協程,代碼的控制權就會返回到caller,否則繼續執行協程函數體(function body)。

PS:如果禁用異常,那么生成的代碼里就不會有 try-catch。此時協程的運行效率幾乎等同非協程版的普通函數。

co_await 機制

co_await 操作符是 C++20 新增的一個關鍵字,co_await expr 一般表示等待一個惰性求值的任務,這個任務可能在某個線程執行,也可能在 OS 內核執行,什么時候執行結束不知道,為了性能,我們又不希望阻塞等待這個任務完成,所以就借助 co_await 把協程掛起並返回到 caller,caller 可以繼續做事情,當任務完成之后協程恢復並拿到 co_await 返回的結果。

所以 co_await 一般有這幾個作用:

  • 掛起協程;
  • 返回到 caller;
  • 等待某個任務(可能是 lazy 的,也可能是非 lazy 的)完成之后返回任務的結果。

編譯器會根據 co_await expr 生成這樣的代碼:

{
    auto&& value = <expr>;
    auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
    auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
    if (!awaiter.await_ready()) //是否需要掛起協程
  	{
    	using handle_t = std::experimental::coroutine_handle<P>;
 
    	using await_suspend_result_t = decltype(awaiter.await_suspend(handle_t::from_promise(p)));
 
    	<suspend-coroutine> //掛起協程
 
    	if constexpr (std::is_void_v<await_suspend_result_t>)
    	{
      		awaiter.await_suspend(handle_t::from_promise(p)); //異步(也可能同步)執行task
      		<return-to-caller-or-resumer> //返回給caller
    	}
    	else
    	{
      		static_assert(
         		std::is_same_v<await_suspend_result_t, bool>,
         		"await_suspend() must return 'void' or 'bool'.");
 
      		if (awaiter.await_suspend(handle_t::from_promise(p)))
      		{
        		<return-to-caller-or-resumer>
      		}
    	}
 
    	<resume-point> //task執行完成,恢復協程,這里是協程恢復執行的地方
  	}
 
	return awaiter.await_resume(); //返回task結果
}

這個代碼執行流程就是“協程運行流程圖”中粉紅色部分,從這個生成的代碼可以看到,通過定制 awaiter.await_ready() 的返回值就可以控制是否掛起協程還是繼續執行,返回 false 就會掛起協程,並執行 awaiter.await_suspend,通過 awaiter.await_suspend 的返回值來決定是返回 caller 還是繼續執行。

正是 co_await 的這種機制是變“異步回調”為“同步”的關鍵。

C++20 協程中最重要的兩個對象就是 promise 對象(恢復協程和獲取某個任務的執行結果)和 awaiter(掛起協程,等待task執行完成),其它的都是“工具人”,要實現想要的的協程,關鍵是要設計如何讓這兩個對象協作好。

例子

#include <iostream>
#include <coroutine>
#include <thread>

namespace Coroutine {
    struct task {
        struct promise_type {
            promise_type() {
                std::cout << "1.task-promise_type():create promise object\n";
            }

            task get_return_object() {
                std::cout << "2.task-get_return_object():create coroutine return object, and the coroutine is created now\n";
                return {std::coroutine_handle<task::promise_type>::from_promise(*this)};
            }

            // initial_suspend()決定協程初始化后,是繼續直接繼續執行協程,還是掛起協程返回caller
            // 返回std::suspend_never,表示不掛起協程,會繼續執行協程函數體(coroutine body)
            // 返回std::suspend_always,表示掛起協程,不會去執行coroutine body,程序的執行返回到caller那里
            std::suspend_never initial_suspend() {
                std::cout << "3.task-initial_suspend():do you want to susupend the current coroutine?\n";
                std::cout << "4.task-initial_suspend():don't suspend because return std::suspend_never, so continue to execute coroutine body\n";
                return {};
            }

            // 調用完void return_void()或者void return_value(T v)后,就會調用final_suspend()
            // 如果final_suspend返回std::suspend_never表示不掛起協程,那么協程就會自動銷毀,先后銷毀promise, 協程幀上得參數和協程幀;
            // 如果返回std::suspend_always則不會自動銷毀協程,需要用戶手動去刪除協程。
            std::suspend_never final_suspend() noexcept {
                std::cout << "15.task-final_suspend():coroutine body finished, do you want to susupend the current coroutine?\n";
                std::cout << "16.task-final_suspend():don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye\n";
                return {};
            }

            // 如果協程是void沒有返回值,那么就需要定義void return_void()
            // 如果有返回值那么就定義void return_value(T v),用來保存協程的返回值
            // return_value或者return_void,這兩個方法只允許存在一個
            void return_void() {
                std::cout << "14.task-return_void():coroutine don't return value, so return_void is called\n";
            }

            void unhandled_exception() {}
        };

        std::coroutine_handle<task::promise_type> handle_;
    };

    struct awaiter {
        // 調用co_wait awaiter{};時調用await_ready()
        // 表示是否准備好,要不要掛起協程
        // await_ready()返回false一般表示要掛起協程,並執行await_suspend
        // 返回true說明協程已經執行完了,這時候調用await_resume返回協程的結果。
        bool await_ready() {
            std::cout << "6.await_ready():do you want to suspend current coroutine?\n";
            std::cout << "7.await_ready():yes, suspend becase awaiter.await_ready() return false\n";
            return false;
        }

        //await_suspend 的返回值來決定是返回 caller 還是繼續執行。
        //返回void:協程執行權交還給當前協程的caller。當前協程在未來某個時機被resume之后,然后執行協程函數中co_await下面的語句
        //返回true:同返回void。
        //返回false:直接執行await_resume
        void await_suspend(std::coroutine_handle<task::promise_type> handle) {
            std::cout << "8.await_suspend(std::coroutine_handle<task::promise_type> handle):execute awaiter.await_suspend()\n";
            std::thread([handle]() mutable {
                std::cout << "11.lambada():resume coroutine to execute coroutine body\n";
                handle();//等價於handle.resume();
                std::cout << "17.lambada():over\n";
            }).detach();
            std::cout << "9.await_suspend(std::coroutine_handle<task::promise_type> handle):a new thread lauched, and will return back to caller\n";
        }

        //調用完await_resume后直接執行協程函數中co_await下面的語句
        void await_resume() {
            std::cout << "12.await_resume()\n";
        }
    };

    task test() {
        std::cout << "5.test():begin to execute coroutine body, the thread id=" << std::this_thread::get_id() << ",and call co_await awaiter{};\n"; //#1
        co_await awaiter{};
        std::cout << "13.test():coroutine resumed, continue execute coroutine body now, the thread id=" << std::this_thread::get_id() << "\n"; //#3
    }

    template<typename T>
    struct lazy {
    public:
        struct promise_type;

        lazy(std::coroutine_handle<promise_type> handle) : m_handle(handle) {
            std::cout << "3.lazy(std::coroutine_handle<promise_type> handle):Construct a lazy object" << std::endl;
        }

        ~lazy() {
            std::cout << "15.~lazy():Destruct a lazy object " << std::endl;
            m_handle.destroy();
        }

        T get() {
            std::cout << "6.lazy.get():I want to execute the coroutine now. call m_handle.resume()" << std::endl;
            if (!m_handle.done()) {
                m_handle.resume();
            }
            std::cout << "13.lazy.get():We got the return value...:" << m_handle.promise().value << std::endl;
            return m_handle.promise().value;
        }

        struct promise_type {
            T value = {};

            promise_type() {
                std::cout << "1.lazy-promise_type():Promise created" << std::endl;
            }

            ~promise_type() {
                std::cout << "16.lazy- ~promise_type():Promise died" << std::endl;
            }

            auto get_return_object() {
                std::cout << "2.lazy-get_return_object():create coroutine return object, and the coroutine is created now" << std::endl;
                return lazy<T>{std::coroutine_handle<promise_type>::from_promise(*this)};
            }

            auto initial_suspend() {
                std::cout << "4.lazy-initial_suspend():Started the coroutine" << std::endl;
                return std::suspend_always{};
            }

            auto final_suspend() noexcept {
                std::cout << "12.lazy-final_suspend():Finished the coroutine" << std::endl;
                return std::suspend_always{};
            }

            void return_value(T v) {
                std::cout << "11.lazy-return_value(T v):Got coroutine result " << v << std::endl;
                value = v;
            }

            void unhandled_exception() {
                std::exit(1);
            }

            //協程體中調用co_yield xxx;的時候調用yield_value(T val)
            auto yield_value(T val) {
                std::cout << "9.lazy-yield_value(T val): " << val << std::endl;
                value = val;

                //后續不再掛起協程,繼續執行
                return std::suspend_never();

//                //后續繼續掛起協程
//                return std::suspend_always();
            }

        };

        std::coroutine_handle<promise_type> m_handle;
    };

    lazy<int> my_coroutine() {
        std::cout << "7.my_coroutine():Execute the coroutine function body" << std::endl;
        std::cout << "8.my_coroutine():call---co_yield 66;" << std::endl;
        co_yield 66;
        std::cout << "10.my_coroutine():call---co_return 88;" << std::endl;
        co_return 88;
    }
} // namespace Coroutine

int main() {
    Coroutine::test();
    std::cout << "10.main():come back to caller becuase of co_await awaiter\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));

    std::cout << "-----------------------------------" << std::endl;

    auto coro = Coroutine::my_coroutine();
    std::cout << "5.main():call coro.get()" << std::endl;
    auto result = coro.get();
    std::cout << "14.main():The coroutine result: " << result << std::endl;

//    std::cout << "main():Second call coro.get()  " << std::endl;
//    result = coro.get();
//    std::cout << "main():The coroutine result2: " << result << std::endl;

    return 0;
}

輸出結果:

1.task-promise_type():create promise object
2.task-get_return_object():create coroutine return object, and the coroutine is created now
3.task-initial_suspend():do you want to susupend the current coroutine?
4.task-initial_suspend():don't suspend because return std::suspend_never, so continue to execute coroutine body
5.test():begin to execute coroutine body, the thread id=1,and call co_await awaiter{};
6.await_ready():do you want to suspend current coroutine?
7.await_ready():yes, suspend becase awaiter.await_ready() return false
8.await_suspend(std::coroutine_handle<task::promise_type> handle):execute awaiter.await_suspend()
9.await_suspend(std::coroutine_handle<task::promise_type> handle):a new thread lauched, and will return back to caller
10.main():come back to caller becuase of co_await awaiter
11.lambada():resume coroutine to execute coroutine body
12.await_resume()
13.test():coroutine resumed, continue execute coroutine body now, the thread id=2
14.task-return_void():coroutine don't return value, so return_void is called
15.task-final_suspend():coroutine body finished, do you want to susupend the current coroutine?
16.task-final_suspend():don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye
17.lambada():over
-----------------------------------
1.lazy-promise_type():Promise created
2.lazy-get_return_object():create coroutine return object, and the coroutine is created now
3.lazy(std::coroutine_handle<promise_type> handle):Construct a lazy object
4.lazy-initial_suspend():Started the coroutine
5.main():call coro.get()
6.lazy.get():I want to execute the coroutine now. call m_handle.resume()
7.my_coroutine():Execute the coroutine function body
8.my_coroutine():call---co_yield 66;
9.lazy-yield_value(T val): 66
10.my_coroutine():call---co_return 88;
11.lazy-return_value(T v):Got coroutine result 88
12.lazy-final_suspend():Finished the coroutine
13.lazy.get():We got the return value...:88
14.main():The coroutine result: 88
15.~lazy():Destruct a lazy object 
16.lazy- ~promise_type():Promise died

Process finished with exit code 0

參考:
1.https://blog.csdn.net/csdnnews/article/details/124123024
2.http://purecpp.org/detail?id=2270
3.http://purecpp.org/detail?id=2278
4.http://purecpp.org/detail?id=2275


免責聲明!

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



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