天天看點

C++20-協程(coroutine)

原文: https://en.cppreference.com/w/cpp/language/coroutines

Coroutines

A coroutine is a function that can suspend execution to be resumed later. Coroutines are stackless: they suspend execution by returning to the caller and the data that is required to resume execution is stored separately from the stack. This allows for sequential code that executes asynchronously (e.g. to handle non-blocking I/O without explicit callbacks), and also supports algorithms on lazy-computed infinite sequences and other uses.

A function is a coroutine if its definition does any of the following:

coroutine是一個可以被挂起和恢複的函數. 協程是無堆棧的:它們通過傳回到調用者來暫停執行,恢複執行所需的資料與堆棧分開存儲。這允許異步執行的順序代碼(例如,在沒有顯式回調的情況下處理非阻塞I/O),也支援延遲計算無限序列的算法和其他用途。

如果一個函數的定義有以下任何一種情況,那麼它就是協程:

  • 使用

    co_await

    操作符暫停執行,直到恢複
    task<> tcp_echo_server() {
      char data[1024];
      for (;;) {
        size_t n = co_await socket.async_read_some(buffer(data));
        co_await async_write(socket, buffer(data, n));
      }
    }
               
  • 使用關鍵字

    co_yield

    暫停執行,傳回一個值
    generator<int> iota(int n = 0) {
      while(true)
        co_yield n++;
    }
               
  • 使用關鍵字

    co_return

    完成執行,傳回一個值
    lazy<int> f() {
    	  co_return 7;
    	}
               

每個協程都必須有一個傳回類型來滿足以下的許多要求。

Restrictions

Coroutines cannot use variadic arguments, plain return statements, or placeholder return types (auto or Concept).

Constexpr functions, constructors, destructors, and the main function cannot be coroutines.

限制條件:

協程不能使用可變參數( variadic arguments)、普通傳回(

return

)語句或

占位符傳回類型

(auto或Concept)。Constexpr函數、

構造函數

析構函數

main函數

不能是協程。

執行

Execution

Each coroutine is associated with

  • the

    promise

    object, manipulated from inside the coroutine. The coroutine submits its result or exception through this object.
  • the

    coroutine handle

    , manipulated from outside the coroutine. This is a non-owning handle used to resume execution of the coroutine or to destroy the coroutine frame.
  • the

    coroutine state

    , which is an internal, heap-allocated (unless the allocation is optimized out), object that contains
    • the promise object
    • the parameters (all copied by value)
    • some representation of the current suspension point, so that resume knows where to continue and destroy knows what local variables were in scope
    • local variables and temporaries whose lifetime spans the current suspension point

每個coroutine的關聯對象:

  • promise

    對象,從協程内部操縱。協程通過此對象送出其結果或異常。
  • corotine handle

    (協程句柄),從協程外部操縱。這是一個非所有者(non-owning)句柄,用于恢複協程的執行或銷毀協程幀。
  • coroutine state

    (協程狀态),它是一個内部的堆配置設定對象(除非配置設定被優化),包含:
    • promise

      對象
    • 參數(都是通過值拷貝)
    • 目前挂起點的一些标記資訊(representation),這樣resume就知道在哪裡繼續,destroy就知道哪些局部變量在作用域中
    • 生存期跨越目前挂起點的局部變量和臨時變量
When a coroutine begins execution, it performs the following:
  • allocates the coroutine state object using

    operator new

    (see below)
  • copies all function parameters to the coroutine state: by-value parameters are moved or copied, by-reference parameters remain references (and so may become dangling if the coroutine is resumed after the lifetime of referred object ends)
  • calls the constructor for the promise object. If the promise type has a constructor that takes all coroutine parameters, that constructor is called, with post-copy coroutine arguments. Otherwise the default constructor is called.
  • calls

    promise.get_return_object()

    and keeps the result in a local variable. The result of that call will be returned to the caller when the coroutine first suspends. Any exceptions thrown up to and including this step propagate back to the caller, not placed in the promise.
  • calls

    promise.initial_suspend()

    and

    co_awaits

    its result. Typical Promise types either return a suspend_always, for lazily-started coroutines, or suspend_never, for eagerly-started coroutines.
  • when

    co_await promise.initial_suspend()

    resumes, starts executing the body of the coroutine

當協程開始執行時,它會執行以下操作:

  • 使用

    operator new

    配置設定協程狀态對象(見下文)
  • 将所有函數形參複制到協程狀态:如果是按值傳參則其被移動(move)或複制,如果是引用傳參則保留引用(是以,如果在被引用對象的生命周期結束後恢複協程,可能會變得懸空, 是以, 程式員注意對象的生命周期)
  • 調用promise對象的構造函數。如果promise類型有一個接受所有協程參數的構造函數,則調用該構造函數,并帶有複制後的協程參數。否則,将調用預設構造函數。
  • 調用

    promise.get_return_object()

    并将結果儲存在一個局部變量中。當協程第一次挂起時,該調用的結果将傳回給調用者。到此步驟為止抛出的任何異常(包括此步驟)都會傳播回調用者,而不是放在promise中。
  • 調用

    promise.initial_suspend()

    co_await

    其結果。典型的Promise類型要麼為lazily-started(慢啟動)協程傳回一個

    suspend_always

    ,要麼為eagerly-started(急啟動)協程傳回一個

    suspend_never

  • co_await promise.initial_suspend()

    恢複時,開始執行協程體
When a coroutine reaches a suspension point
  • the return object obtained earlier is returned to the caller/resumer, after implicit conversion to the return type of the coroutine, if necessary.
When a coroutine reaches the

co_return

statement, it performs the following:
  • calls promise.return_void() for
    • co_return

      ;
    • co_return expr

      where expr has type void
    • falling off the end of a void-returning coroutine. The behavior is undefined if the Promise type has no

      Promise::return_void()

      member function in this case.
  • or calls

    promise.return_value(expr)

    for

    co_return

    expr where expr has non-void type
  • destroys all variables with automatic storage duration in reverse order they were created.
  • calls

    promise.final_suspend()

    and

    co_awaits

    the result.
If the coroutine ends with an uncaught exception, it performs the following:
  • catches the exception and calls

    promise.unhandled_exception()

    from within the catch-block
  • calls

    promise.final_suspend()

    and

    co_awaits

    the result (e.g. to resume a continuation or publish a result). It’s undefined behavior to resume a coroutine from this point.
When the coroutine state is destroyed either because it terminated via

co_return

or uncaught exception, or because it was destroyed via its handle, it does the following:
  • calls the destructor of the promise object.
  • calls the destructors of the function parameter copies.
  • calls

    operator delete

    to free the memory used by the coroutine state
  • transfers execution back to the caller/resumer.

當協程到達一個暫停點時

  • 如果需要,在隐式轉換為協程的傳回類型之後,前面獲得的傳回對象傳回給caller/resumer。

當協程到達

co_return

語句時,它執行以下操作:

  • 調用

    promise.return_void()

    • co_return

      ;
    • co_return expr

      其中

      expr

      void

      類型
    • 從傳回空值的協程的末尾脫落。在這種情況下,如果Promise類型沒有

      Promise::return_void()

      成員函數,則該行為是未定義(undefined的。
  • 或者調用

    promise.return_value(expr)

    來擷取

    co_return expr

    ,其中expr為非void類型
  • 按建立時的相反順序銷毀所有自動變量。
  • 調用

    promise.final_suspend()

    co_await

    結果。

如果協程以未捕獲的異常結束,它将執行以下操作:

  • 捕獲異常并在catch塊中調用

    promise.unhandled_exception()

  • 調用

    promise.final_suspend()

    co_await

    結果(例如恢複延續或釋出結果)。從這一點恢複協程是未定義的行為。

當協程狀态被銷毀是因為它通過

co_return

或未捕獲的異常終止,或因為它是通過它的句柄銷毀的,它會執行以下操作:

  • 調用

    promise

    對象的析構函數。
  • 調用函數參數副本的析構函數。
  • 調用

    operator delete

    來釋放協程狀态所使用的記憶體
  • 将執行傳輸回caller/resumer。

堆配置設定

Heap allocation

coroutine state is allocated on the heap via non-array operator new.

If the Promise type defines a class-level replacement, it will be used, otherwise global operator new will be used.

If the Promise type defines a placement form of operator new that takes additional parameters, and they match an argument list where the first argument is the size requested (of type std::size_t) and the rest are the coroutine function arguments, those arguments will be passed to operator new (this makes it possible to use leading-allocator-convention for coroutines)

The call to operator new can be optimized out (even if custom allocator is used) if

  • The lifetime of the coroutine state is strictly nested within the lifetime of the caller, and
  • the size of coroutine frame is known at the call site

in that case, coroutine state is embedded in the caller’s stack frame (if the caller is an ordinary function) or coroutine state (if the caller is a coroutine)

If allocation fails, the coroutine throws std::bad_alloc, unless the Promise type defines the member function Promise::get_return_object_on_allocation_failure(). If that member function is defined, allocation uses the nothrow form of operator new and on allocation failure, the coroutine immediately returns the object obtained from Promise::get_return_object_on_allocation_failure() to the caller.

協程狀态是通過非數組操作符new在堆上配置設定的。

如果Promise類型定義了類級别的

operator new

,則使用它,否則将使用全局

operator new

如果Promise類型定義了一個需要額外的參數的

operator new

作為替代,和他們比對一個參數清單,第一個參數是請求的大小(類型的std:: size_t),其餘是協同程式函數參數,這些參數将傳遞給

operator new

的(這使它可以使用leading-allocator-convention協程)

對operator new的調用可以優化出來(即使使用了自定義配置設定器),如果:

  • 協程狀态的生存期嚴格嵌套在調用者的生存期内,并且
  • 協程幀的大小在調用站點是已知的

    在這種情況下,協程狀态被嵌入到調用者的堆棧架構中(如果調用者是一個普通函數)或協程狀态(如果調用者是一個協程)

如果配置設定失敗,則該coroutine将抛出

std::bad_alloc

,除非Promise類型定義了成員函數

Promise::get_return_object_on_allocation_failure()

。如果定義了該成員函數,則allocation使用

operator new

nothrow

形式,并且在配置設定失敗時,協程立即将

Promise::get_return_object_on_allocation_failure()

獲得的對象傳回給調用者。

Promise

The Promise type is determined by the compiler from the return type of the coroutine using

std::coroutine_traits

.

Formally, let R and Args… denote the return type and parameter type list of a coroutine respectively, ClassT and /cv-qual/ (if any) denote the class type to which the coroutine belongs and its cv-qualification respectively if it is defined as a non-static member function, its Promise type is determined by:

  • std::coroutine_traits<R, Args...>::promise_type

    , if the coroutine is not defined as a non-static member function,
  • std::coroutine_traits<R, ClassT &, Args...>::promise_type

    , if the coroutine is defined as a non-static member function that is not rvalue-reference-qualified,
  • std::coroutine_traits<R, ClassT &&, Args...>::promise_type

    , if the coroutine is defined as a non-static member function that is rvalue-reference-qualified.
For example:
  • If the coroutine is defined as

    task<float> foo(std::string x, bool flag);

    , then its Promise type is

    std::coroutine_traits<task<float>, std::string, bool>::promise_type

    .
  • If the coroutine is defined as

    task<void> my_class::method1(int x) const

    ;, its Promise type is

    std::coroutine_traits<task<void>, const my_class&, int>::promise_type

    .
  • If the coroutine is defined as

    task<void> my_class::method1(int x) &&;

    , its Promise type is

    std::coroutine_traits<task<void>, my_class&&, int>::promise_type

    .

Promise類型由編譯器根據使用

std::coroutine_traits

的協程傳回類型确定。正式地,設

R

Args…分别表示協程的傳回類型和參數類型清單,

classsT`和/cv-qual/(如果有的話)分别表示協程所屬的類類型和它的cv限定條件。如果它被定義為一個非靜态成員函數,它的Promise類型由:

  • std::coroutine_traits<R, Args...>::promise_type

    ,如果協程未定義為非靜态成員函數,
  • std::coroutine_traits<R, ClassT &, Args...>::promise_type

    , 如果協程定義為非rvalue-reference限定的非靜态成員函數,
  • std::coroutine_traits<R, ClassT &&, Args...>::promise_type

    , 如果協程定義為rvalue-reference限定的非靜态成員函數。

舉例:

  • 如果coroutine被定義為

    task<float> foo(std::string x, bool flag);

    , 那麼它的Promise類型是

    std::coroutine_traits<task<float>, std::string, bool>::promise_type

    .
  • 如果coroutine被定義為

    task<void> my_class::method1(int x) const

    ;, 那麼它的Promise類型是

    std::coroutine_traits<task<void>, const my_class&, int>::promise_type

    .
  • 如果coroutine被定義為

    task<void> my_class::method1(int x) &&;

    , 那麼它的Promise類型是

    std::coroutine_traits<task<void>, my_class&&, int>::promise_type

    .

co_await

The unary operator co_await suspends a coroutine and returns control to the caller. Its operand is an expression whose type must either define operator co_await, or be convertible to such type by means of the current coroutine’s

Promise::await_transform

一進制操作符

co_await

挂起協程并将控制權傳回給調用者。它的操作數是一個表達式,其類型必須定義操作符

co_await

,或者通過目前協程的

Promise::await_transform

可轉換為該類型

co_await expr		
           
First, expr is converted to an awaitable as follows:
  • if expr is produced by an initial suspend point, a final suspend point, or a yield expression, the awaitable is expr, as-is.
  • otherwise, if the current coroutine’s Promise type has the member function await_transform, then the awaitable is promise.await_transform(expr)
  • otherwise, the awaitable is expr, as-is.

首先,expr被轉換為可等待對象,如下所示:

  • 如果expr是由初始挂起點、最終挂起點或yield表達式生成的,則可等待對象按實際情況為expr。
  • 否則,如果目前協程的Promise類型有成員函數await_transform,那麼可等待對象就是Promise .await_transform(expr)
  • 否則,可等待對象就是expr。
If the expression above is a prvalue, the awaiter object is a temporary materialized from it. Otherwise, if the expression above is an glvalue, the awaiter object is the object to which it refers.

如果上面的表達式是prvalue,則awaiter對象是它的臨時實體化對象。否則,如果上面的表達式是glvalue,則awaiter對象就是它所引用的對象。

Then, awaiter.await_ready() is called (this is a short-cut to avoid the cost of suspension if it’s known that the result is ready or can be completed synchronously). If its result, contextually-converted to bool is false then

然後,調用

await .await_ready()

如果知道結果已經就緒或可以同步完成,這是一種避免挂起代價的捷徑)。如果它的結果,上下文轉換為bool則為false

The coroutine is suspended (its coroutine state is populated with local variables and current suspension point).

awaiter.await_suspend(handle) is called, where handle is the coroutine handle representing the current coroutine. Inside that function, the suspended coroutine state is observable via that handle, and it’s this function’s responsibility to schedule it to resume on some executor, or to be destroyed (returning false counts as scheduling)

協程被挂起(它的協程狀态由局部變量和目前挂起點填充)。調用await .await_suspend(句柄),其中句柄是表示目前協程的協程句柄。在這個函數内部,挂起的協程狀态是可以通過這個句柄觀察到的,這個函數的責任是安排它在某些執行器上恢複,或被銷毀(傳回錯誤計數作為排程)。

  • if await_suspend returns void, control is immediately returned to the caller/resumer of the current coroutine (this coroutine remains suspended), otherwise
  • if await_suspend returns bool,
    • the value true returns control to the caller/resumer of the current coroutine
    • the value false resumes the current coroutine.
  • if await_suspend returns a coroutine handle for some other coroutine, that handle is resumed (by a call to handle.resume()) (note this may chain to eventually cause the current coroutine to resume)
  • if await_suspend throws an exception, the exception is caught, the coroutine is resumed, and the exception is immediately re-thrown
  • 如果await_suspend傳回void,則控制權立即傳回給目前協程的調用者/恢複者(該協程保持挂起狀态),否則
    • 如果await_suspend傳回bool值,
    • 值true将控制權傳回給目前協程的調用者/恢複者
    • 如果值為false,則恢複目前協程。
  • 如果await_suspend傳回其他協程的協程句柄,該句柄将被恢複(通過調用handle.resume())(注意這可能導緻目前協程最終恢複)
  • 如果await_suspend抛出異常,異常被捕獲,協程被恢複,異常立即被重新抛出
Finally, awaiter.await_resume() is called, and its result is the result of the whole

co_await expr

expression.

If the coroutine was suspended in the co_await expression, and is later resumed, the resume point is immediately before the call to awaiter.await_resume().

最後,調用

await .await_resume()

,其結果是整個

co_await expr

表達式的結果。

如果協程在

co_await

表達式中被挂起,然後被恢複,恢複點就在調用

await .await_resume()

之前。

Note that because the coroutine is fully suspended before entering awaiter.await_suspend(), that function is free to transfer the coroutine handle across threads, with no additional synchronization. For example, it can put it inside a callback, scheduled to run on a threadpool when async I/O operation completes. In that case, since the current coroutine may have been resumed and thus executed the awaiter object’s destructor, all concurrently as

await_suspend()

continues its execution on the current thread,

await_suspend()

should treat

*this

as destroyed and not access it after the handle was published to other threads.

注意,因為協程在進入

await .await_suspend()

之前已經完全挂起,是以該函數可以自由地跨線程傳遞協程句柄,而不需要額外的同步操作。例如,它可以将其放在回調函數中,計劃在異步I/O操作完成時線上程池中運作。在這種情況下,因為目前的協同程式可能已經恢複,是以等待對象的析構函數執行,所有并發

await_suspend()

在目前線程繼續執行,

await_suspend()

應該把

*this

當作已經銷毀并且在将句柄釋出到其他線程之後不要再去通路它。

例子

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
 
auto switch_to_new_thread(std::jthread& out) {
  struct awaitable {
    std::jthread* p_out;
    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle<> h) {
      std::jthread& out = *p_out;
      if (out.joinable())
        throw std::runtime_error("Output jthread parameter not empty");
      out = std::jthread([h] { h.resume(); });
      // Potential undefined behavior: accessing potentially destroyed *this
      // std::cout << "New thread ID: " << p_out->get_id() << '\n';
      std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
    }
    void await_resume() {}
  };
  return awaitable{&out};
}
 
struct task{
  struct promise_type {
    task get_return_object() { return {}; }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() {}
  };
};
 
task resuming_on_new_thread(std::jthread& out) {
  std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
  co_await switch_to_new_thread(out);
  // awaiter destroyed here
  std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}
 
int main() {
  std::jthread out;
  resuming_on_new_thread(out);
}
           

需要使用

gcc 10.2

進行編譯

$ g++ --version
g++ (Ubuntu 10.2.0-5ubuntu1~20.04) 10.2.0
$ g++ coroutine.cc -std=c++20 -fcoroutines
$ ./a.out 
Coroutine started on thread: 140421255046976
New thread ID: 140421255042816
Coroutine resumed on thread: 140421255042816
           

This section is incomplete

Reason: examples

這一部分尚未完成

co_yield

co_yield

Yield-expression returns a value to the caller and suspends the current coroutine: it is the common building block of resumable generator functions

co_yield expr

co_yield braced-init-list

It is equivalent to

co_await promise.yield_value(expr)

A typical generator’s yield_value would store (copy/move or just store the address of, since the argument’s lifetime crosses the suspension point inside the co_await) its argument into the generator object and return std::suspend_always, transferring control to the caller/resumer.

Yield-expression傳回一個值給調用者,并挂起目前協程:它可以建構可恢複生成器函數(類似python中的

yield

)

co_yield expr		
co_yield braced-init-list	
           

它等價于

co_await promise.yield_value(expr)
           

一個典型的生成器的

yield_value

将其參數存儲(複制/移動或僅僅存儲其位址,因為參數的生命周期跨越了

co_await

内部的懸挂點)到生成器對象中,并傳回

std:: susend_always

,将控制權轉移給caller/resumer。

This section is incomplete

Reason: examples

這一部分尚未完成