原文: 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
object, manipulated from inside the coroutine. The coroutine submits its result or exception through this object.
promise
- the
, manipulated from outside the coroutine. This is a non-owning handle used to resume execution of the coroutine or to destroy the coroutine frame.
coroutine handle
- the
, which is an internal, heap-allocated (unless the allocation is optimized out), object that contains
coroutine state
- 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
-
(協程句柄),從協程外部操縱。這是一個非所有者(non-owning)句柄,用于恢複協程的執行或銷毀協程幀。corotine handle
-
(協程狀态),它是一個内部的堆配置設定對象(除非配置設定被優化),包含:coroutine state
-
對象promise
- 參數(都是通過值拷貝)
- 目前挂起點的一些标記資訊(representation),這樣resume就知道在哪裡繼續,destroy就知道哪些局部變量在作用域中
- 生存期跨越目前挂起點的局部變量和臨時變量
-
When a coroutine begins execution, it performs the following:
- allocates the coroutine state object using
(see below)
operator new
- 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
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.
promise.get_return_object()
- calls
and
promise.initial_suspend()
its result. Typical Promise types either return a suspend_always, for lazily-started coroutines, or suspend_never, for eagerly-started coroutines.
co_awaits
- when
resumes, starts executing the body of the coroutine
co_await promise.initial_suspend()
當協程開始執行時,它會執行以下操作:
- 使用
配置設定協程狀态對象(見下文)operator new
- 将所有函數形參複制到協程狀态:如果是按值傳參則其被移動(move)或複制,如果是引用傳參則保留引用(是以,如果在被引用對象的生命周期結束後恢複協程,可能會變得懸空, 是以, 程式員注意對象的生命周期)
- 調用promise對象的構造函數。如果promise類型有一個接受所有協程參數的構造函數,則調用該構造函數,并帶有複制後的協程參數。否則,将調用預設構造函數。
- 調用
并将結果儲存在一個局部變量中。當協程第一次挂起時,該調用的結果将傳回給調用者。到此步驟為止抛出的任何異常(包括此步驟)都會傳播回調用者,而不是放在promise中。promise.get_return_object()
- 調用
和promise.initial_suspend()
其結果。典型的Promise類型要麼為lazily-started(慢啟動)協程傳回一個co_await
,要麼為eagerly-started(急啟動)協程傳回一個suspend_always
。suspend_never
- 當
恢複時,開始執行協程體co_await promise.initial_suspend()
When a coroutine reaches a suspension pointWhen a coroutine reaches the
- the return object obtained earlier is returned to the caller/resumer, after implicit conversion to the return type of the coroutine, if necessary.
statement, it performs the following:
co_return
If the coroutine ends with an uncaught exception, it performs the following:
- calls promise.return_void() for
;
co_return
where expr has type void
co_return expr
- falling off the end of a void-returning coroutine. The behavior is undefined if the Promise type has no
member function in this case.
Promise::return_void()
- or calls
for
promise.return_value(expr)
expr where expr has non-void type
co_return
- destroys all variables with automatic storage duration in reverse order they were created.
- calls
and
promise.final_suspend()
the result.
co_awaits
When the coroutine state is destroyed either because it terminated via
- catches the exception and calls
from within the catch-block
promise.unhandled_exception()
- calls
and
promise.final_suspend()
the result (e.g. to resume a continuation or publish a result). It’s undefined behavior to resume a coroutine from this point.
co_awaits
or uncaught exception, or because it was destroyed via its handle, it does the following:
co_return
- calls the destructor of the promise object.
- calls the destructors of the function parameter copies.
- calls
to free the memory used by the coroutine state
operator delete
- transfers execution back to the caller/resumer.
當協程到達一個暫停點時
- 如果需要,在隐式轉換為協程的傳回類型之後,前面獲得的傳回對象傳回給caller/resumer。
當協程到達
co_return
語句時,它執行以下操作:
- 調用
promise.return_void()
-
;co_return
-
其中co_return expr
是expr
類型void
- 從傳回空值的協程的末尾脫落。在這種情況下,如果Promise類型沒有
成員函數,則該行為是未定義(undefined的。Promise::return_void()
-
- 或者調用
來擷取promise.return_value(expr)
,其中expr為非void類型co_return expr
- 按建立時的相反順序銷毀所有自動變量。
- 調用
和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:
For example:
, if the coroutine is not defined as a non-static member function,
std::coroutine_traits<R, 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.
std::coroutine_traits<R, ClassT &&, Args...>::promise_type
- If the coroutine is defined as
, then its Promise type is
task<float> foo(std::string x, bool flag);
.
std::coroutine_traits<task<float>, std::string, bool>::promise_type
- If the coroutine is defined as
;, its Promise type is
task<void> my_class::method1(int x) const
.
std::coroutine_traits<task<void>, const my_class&, int>::promise_type
- If the coroutine is defined as
, its Promise type is
task<void> my_class::method1(int x) &&;
.
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
-
, 如果協程定義為非rvalue-reference限定的非靜态成員函數,std::coroutine_traits<R, ClassT &, Args...>::promise_type
-
, 如果協程定義為rvalue-reference限定的非靜态成員函數。std::coroutine_traits<R, ClassT &&, Args...>::promise_type
舉例:
- 如果coroutine被定義為
, 那麼它的Promise類型是task<float> foo(std::string x, bool flag);
.std::coroutine_traits<task<float>, std::string, bool>::promise_type
- 如果coroutine被定義為
;, 那麼它的Promise類型是task<void> my_class::method1(int x) const
.std::coroutine_traits<task<void>, const my_class&, int>::promise_type
- 如果coroutine被定義為
, 那麼它的Promise類型是task<void> my_class::method1(int x) &&;
.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 ascontinues its execution on the current thread,
await_suspend()
should treat
await_suspend()
as destroyed and not access it after the handle was published to other threads.
*this
注意,因為協程在進入
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
這一部分尚未完成