目录
协程(Coroutines)
协程(Coroutines )是一个可以挂起执行以便稍后恢复的函数。协程是无堆栈的:它们通过返回到调用方来暂停执行,并且恢复执行所需的数据与堆栈分开存储。这允许异步执行的顺序代码(例如,在没有显式回调的情况下处理非阻塞I/O),还支持惰性计算无限序列上的算法和其他用途。
如果函数的定义包含以下任何一项,则该函数就是协程:
- co_await表达式 — 暂停执行直到恢复
task<> tcp_echo_server()
{
char data[1024];
while (true)
{
std::size_t n = co_await socket.async_read_some(buffer(data));
co_await async_write(socket, buffer(data, n));
}
}
- co_yield 表达式 — 暂停执行返回值
generator<unsigned int> iota(unsigned int n = 0)
{
while (true)
co_yield n++;
}
- co_return 语句 — 返回值以完成执行
lazy<int> f()
{
co_return 7;
}
每个协程都必须有一个满足许多要求的返回类型,如下所述。
限制(Restrictions)
协程不能使用可变参数、纯返回语句或占位符返回类型(auto或Concept)。
Const eval函数、const expr函数、构造函数、析构函数和主函数不能是协程。
执行(Execution)
每个协程关联于:
- promise对象,从协程内部进行操作。协程通过此对象提交其结果或异常。Promise对象与std::Promise没有任何关系。
- 从协程程序外部操作的协程程序句柄。这是一个非拥有句柄,用于恢复协程的执行或销毁协程框架。
- 协程状态,它是内部动态分配的存储(除非优化了分配),包含:
- promise对象
- 参数(全部按值复制)
- 当前挂起点的一些表示,这样知道在哪里继续,销毁知道范围内的局部变量.
- 局部变量和临时变量,它们的寿命跨越当前暂停点。
当协程开始执行时,它执行以下操作:
- 使用运算符new分配协程状态对象。
- 将所有函数参数复制到协程状态:按值参数被移动或复制,按引用参数保留引用(因此,如果在引用对象的生存期结束后恢复协程,则可能会变成悬空的——参见下面的示例)。
- 调用promise对象的构造函数。如果promise类型有一个接受所有协程参数的构造函数,则调用该构造函数,并使用复制后的协程参数。否则将调用默认构造函数。
- 调用promise.get_return_object()并将结果保存在局部变量中。当协程首次挂起时,该调用的结果将返回给调用者。在该步骤之前(包括该步骤)引发的任何异常都会传播回调用方,而不是放在promise中。
- 调用promise.initial_suspend(),co_awaits其结果。典型的Promise类型要么返回std::suspend_allways,用于延迟启动的协程,要么返回std::suspend_never,用于急切启动的协程。
- 当co-await promise.initial_suspend()恢复时,开始执行协程的主体。
参数悬空的一些示例:
#include <coroutine>
#include <iostream>
struct promise;
struct coroutine : std::coroutine_handle<promise>
{
using promise_type = ::promise;
};
struct promise
{
coroutine get_return_object() { return {coroutine::from_promise(*this)}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
struct S
{
int i;
coroutine f()
{
std::cout << i;
co_return;
}
};
void bad1()
{
coroutine h = S{0}.f();
// S{0} destroyed
// S{0} 已销毁
h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
// 恢复的协程执行std::cout << i ,释放后使用S::i
h.destroy();
}
coroutine bad2()
{
S s{0};
return s.f(); // returned coroutine can't be resumed without committing use after free
// 返回的协程在释放后未提交使用就无法恢复
}
void bad3()
{
coroutine h = [i = 0]() -> coroutine // a lambda that's also a coroutine
// 一个lambda也是一个协程
{
std::cout << i;
co_return;
}(); // immediately invoked
// 立即调用
// lambda destroyed
// lambda已销毁
h.resume(); // uses (anonymous lambda type)::i after free
// 释放后使用(匿名lambda类型)::i
h.destroy();
}
void good()
{
coroutine h = [](int i) -> coroutine // make i a coroutine parameter
// 使i成为协程参数
{
std::cout << i;
co_return;
}(0);
// lambda destroyed
// ambda已销毁
h.resume(); // no problem, i has been copied to the coroutine
// frame as a by-value parameter
//没问题,i作为值参数已经被复制到协同程序了
h.destroy();
}
当协程程序达到暂停点时:
- 如果需要,在隐式转换为协程的返回类型之后,将先前获得的返回对象返回给caller/resumer。
当协程到达co_return语句时,它将执行以下操作: - 为下了函数调用promise.return_void():
- co_return
- co_return expr; 其中expr的类型为void
- 或为co_return expr调用promise.return_value(expr);其中expr具有非void类型
- 以与创建顺序相反的顺序销毁所有具有自动存储持续时间的变量。
- 调用promise.final_suspend(),co_awaits结果。
从协程的末尾脱落相当于co_return,除了如果在Promise的范围中找不到return_void的声明,则行为是未定义的。函数体中没有定义关键字的函数不是协程,无论其返回类型如何,如果返回类型不是(可能是cv限定的)void,则从末尾掉下来会导致未定义的行为。
// assuming that task is some coroutine task type
// 假设task是某种协程任务类型
task<void> f()
{
// not a coroutine, undefined behavior
// 不是协程,未定义的行为
}
task<void> g()
{
co_return; // OK
}
task<void> h()
{
co_await g();
// OK, implicit co_return;
// OK, 隐式co_return
}
如果协程程序以未捕获的异常结束,它将执行以下操作:
- 捕获异常并从catch块中调用promise.unhanded_exception()
- 调用promise.final_suspend(),co_wait结果(例如,恢复延续或发布结果)。从这一点开始恢复协程程序是未定义的行为。
当协程状态被破坏时,无论是因为它通过co_return或未捕获异常终止,还是因为它通过其句柄被破坏,它都会执行以下操作: - 调用promise对象的析构函数。
- 调用函数参数副本的析构函数。
- 调用操作符delete来释放协程状态所使用的内存。
- 将执行转移回caller/resumer。
动态分配(Dynamic allocation)
通过非数组运算符new动态分配协程状态。
如果Promise类型定义了类级替换,则将使用它,否则将使用全局运算符new。
如果Promise类型定义了一个采用额外参数的运算符new的放置形式,并且它们与一个参数列表匹配,其中第一个参数是请求的大小(类型为std::size_t),其余的是协程函数参数,则这些参数将传递给运算符new(这使得可以对协程使用前导分配器约定)。
可以优化对运算符new的调用(即使使用了自定义分配器)如果
- 协程状态的生存期严格嵌套在调用程序的生存期内,并且
- 协程框架的大小在调用侧是已知的。
在这种情况下,协程状态嵌入到调用方的堆栈帧中(如果调用方是普通函数)或协程状态中(如果调用者是协程)。
如果分配失败,协程将抛出std::bad_alloc,除非Promise类型定义了成员函数Promise::get_return_object_on_allocation_failure()。如果定义了该成员函数,则分配使用另一种形式的运算符new,并且在分配失败时,协程会立即将从Promise::get_return_object_on_allocation_failure()获得的对象返回给调用方,例如:
struct Coroutine::promise_type
{
/* ... */
// ensure the use of non-throwing operator-new
// 确保使用新的非异常操作符new
static Coroutine get_return_object_on_allocation_failure()
{
std::cerr << __func__ << '\n';
throw std::bad_alloc(); // or, return Coroutine(nullptr);
}
// custom non-throwing overload of new
// 自定义非异常操作符new的重载
void* operator new(std::size_t n) noexcept
{
if (void* mem = std::malloc(n))
return mem;
return nullptr; // allocation failure
// 分配失败
}
};
Promise
Promise类型由编译器根据使用std::coroutine_tracts的协程的返回类型来确定。
正规的,让:
- R和Args…分别表示协程的返回类型和参数类型列表,
- ClassT表示协程所属的类类型,如果它被定义为非静态成员函数,
- cv表示在函数声明中声明的cv资格,如果它被定义为非静态成员函数,
其Promise类型由以下因素决定: - std::coroutine_traits<R, Args…>::promise_type, 如果协程没有被定义为隐式对象成员函数,
- std::coroutine_traits<R, cv ClassT&, Args…>::promise_type,如果协程被定义为不是右值引用限定的隐式对象成员函数,
- std::coroutine_traits<R, cv ClassT&&, Args…>::promise_type,如果协程被定义为一个隐式对象成员函数,该函数是右值引用限定的。
例如:
If the coroutine is defined as … | then its Promise type is … |
---|---|
task foo(int x); | std::coroutine_traits<task, int>::promise_type |
task Bar::foo(int x) const; | std::coroutine_traits<task, const Bar&, int>::promise_type |
task Bar::foo(int x) &&; | std::coroutine_traits<task, Bar&&, int>::promise_type |
co_await
一元运算符co_await挂起协程并将控制权返回给调用者。它的操作数是一个表达式,(1)是定义成员运算符co_await的类类型,或者可以传递给非成员运算符co_await,或者(2)可以通过当前协程的Promise::await_transform转换为这样的类类型。
co_await expr
co_await表达式只能出现在正则函数体(包括lambda表达式的函数体)中的潜在求值表达式中,而不能出现:
- 在handler中
- 在声明语句中,除非它出现在该声明语句的初始值设定项中,
- 在init语句的简单声明中(参见if、switch、for和range for),除非它出现在该init语句的初始值设定项中,
- 在默认参数中,或者
- 在具有静态或线程存储持续时间的块作用域变量的初始值设定项中。
首先,expr转换为awaitable,如下所示:
- 如果expr是由初始挂起点、最终挂起点或yield表达式生成的,那么awaitable就是expr。
- 否则,如果当前协程的Promise类型具有成员函数await_transform,那么awaitable是Promise.await_transform(expr)。
- 否则,awaitable就是expr。
然后,获得awaiter对象,如下所示:
- 如果运算符co_await的重载解析给出单个最佳过载,则awaiter是该调用的结果:
- awaitable.operator co_await()用于成员重载
- operator co_await(static_cast<Awaitable&&>(awaitable))用于非成员重载
- 否则,如果重载解析没有找到运算符co_await,则awaiter是awaitable的。
- 否则,如果重载解析不明确,则表示程序格式不正确。
如果上面的表达式是右值,则awaiter对象是从临时对象构建的。否则,如果上面的公式是glvalue,则awaiter对象就是它所引用的对象。
然后,调用awaiter.await_ready(如果知道结果已经准备好或可以同步完成,这是避免挂起成本的捷径)。如果其上下文转换为布尔的结果为false,则协程被挂起(它的协程状态由局部变量和当前挂接点填充)。
调用awaiter.await_suspend(handle),其中handle是表示当前协程的协程句柄。在该函数中,通过该句柄可以观察到挂起的协程状态,该函数的职责是将其调度为在某个执行器上恢复,或者销毁(返回错误计数作为调度)
- 如果await_suspend返回void,则控制权将立即返回给当前协程的caller/resumer(此协程保持挂起状态),否则
- 如果await_suspend返回bool,
- 值true将控制权返回给当前协同程序的caller/resumer
- 值false将恢复当前的协同程序。
- 如果await_suspend为其他协程返回一个协程句柄,则该句柄将被恢复(通过调用handle.resume())(注意,这可能会连锁,最终导致当前协程恢复)。
- 如果await_suspend抛出异常,则会捕获该异常,恢复协程,并立即重新抛出该异常。
最后,调用awaiter.await_resume()(无论协程是否已挂起),其结果是整个co_await expr表达式的结果。
如果协程在co-await表达式中被挂起,随后又被恢复,则恢复点就在调用awaiter.await_resume()之前。
请注意,由于协程在输入awaiter.await_suspend()之前已完全挂起,因此该函数可以自由地跨线程传输协程句柄,而无需额外的同步。例如,它可以将其放入回调中,计划在异步I/O操作完成时在线程池上运行。在这种情况下,由于当前协程可能已经恢复并因此执行了awaiter对象的析构函数,所有这些都是在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
// 潜在的未定义行为:访问可能被破坏的*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);
}
运行结果:
Coroutine started on thread: 139972277602112
New thread ID: 139972267284224
Coroutine resumed on thread: 139972267284224
注意:awaiter对象是协程状态的一部分(作为一个临时对象,其生存期跨越挂起点),并在co_await表达式完成之前被销毁。它可以用于根据某些异步I/O API的要求维护每个操作的状态,而不需要额外的动态分配。
标准库定义了两个简单的awaitable:std::suspend_allways和std::suspend_never。
此部分不完整原因:示例
Demo of promise_type::await_transform and a program provided awaiter
#include <cassert>
#include <coroutine>
#include <iostream>
struct tunable_coro
{
// An awaiter whose "readiness" is determined via constructor's parameter.
// 通过构造函数的参数确定其“就绪状态”的awaiter。
class tunable_awaiter
{
bool ready_;
public:
explicit(false) tunable_awaiter(bool ready) : ready_{ready} {}
// Three standard awaiter interface functions:
// 三个标准awaiter接口函数:
bool await_ready() const noexcept { return ready_; }
static void await_suspend(std::coroutine_handle<>) noexcept {}
static void await_resume() noexcept {}
};
struct promise_type
{
using coro_handle = std::coroutine_handle<promise_type>;
auto get_return_object() { return coro_handle::from_promise(*this); }
static auto initial_suspend() { return std::suspend_always(); }
static auto final_suspend() noexcept { return std::suspend_always(); }
static void return_void() {}
static void unhandled_exception() { std::terminate(); }
// A user provided transforming function which returns the custom awaiter:
// 一个用户提供的转换函数,它返回自定义的awaiter:
auto await_transform(std::suspend_always) { return tunable_awaiter(!ready_); }
void disable_suspension() { ready_ = false; }
private:
bool ready_{true};
};
tunable_coro(promise_type::coro_handle h) : handle_(h) { assert(h); }
// For simplicity, declare these 4 special functions as deleted:
// 为了简单起见,将这4个特殊函数声明为已删除:
tunable_coro(tunable_coro const&) = delete;
tunable_coro(tunable_coro&&) = delete;
tunable_coro& operator=(tunable_coro const&) = delete;
tunable_coro& operator=(tunable_coro&&) = delete;
~tunable_coro()
{
if (handle_)
handle_.destroy();
}
void disable_suspension() const
{
if (handle_.done())
return;
handle_.promise().disable_suspension();
handle_();
}
bool operator()()
{
if (!handle_.done())
handle_();
return !handle_.done();
}
private:
promise_type::coro_handle handle_;
};
tunable_coro generate(int n)
{
for (int i{}; i != n; ++i)
{
std::cout << i << ' ';
// The awaiter passed to co_await goes to promise_type::await_transform which
// issues tunable_awaiter that initially causes suspension (returning back to
// main at each iteration), but after a call to disable_suspension no suspension
// happens and the loop runs to its end without returning to main().
//传递给co_await的awaiter将转到promise_type::await_transform
//发出最初导致挂起的tunable_awaider(返回到
//每次迭代时的main),但在调用disable_ssuspension后没有挂起
//发生,循环运行到最后而不返回main()。
co_await std::suspend_always{};
}
}
int main()
{
auto coro = generate(8);
coro(); // emits only one first element == 0
for (int k{}; k < 4; ++k)
{
coro(); // emits 1 2 3 4, one per each iteration
std::cout << ": ";
}
coro.disable_suspension();
coro(); // emits the tail numbers 5 6 7 all at ones
}
输出:
0 1 : 2 : 3 : 4 : 5 6 7
co_yield
co_yield表达式向调用方返回一个值并挂起当前的协程:它是可恢复生成器函数的公共构建块。
co_yield expr
co_yield braced-init-list
它相当于:
co_await promise.yield_value(expr)
一个典型的生成器的yield_value会将其参数存储(复制/移动或仅存储的地址,因为参数的生存期跨越了co_await内的挂起点)到生成器对象中,并返回std::suspend_allways,将控制权转移到调用方/返回方。
#include <coroutine>
#include <cstdint>
#include <exception>
#include <iostream>
template<typename T>
struct Generator
{
// The class name 'Generator' is our choice and it is not required for coroutine
// magic. Compiler recognizes coroutine by the presence of 'co_yield' keyword.
// You can use name 'MyGenerator' (or any other name) instead as long as you include
// nested struct promise_type with 'MyGenerator get_return_object()' method.
//类名“Generator”是我们的选择,它不是协程所必需的魔术
//编译器通过关键字“co_yield”的存在来识别协程程序。
//您可以使用名称“MyGenerator”(或任何其他名称),只要您包含
//带有"MyGenerator get_return_object()"方法的嵌套结构promise_type。
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type // required
{
T value_;
std::exception_ptr exception_;
Generator get_return_object()
{
return Generator(handle_type::from_promise(*this));
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); } // saving
// exception
template<std::convertible_to<T> From> // C++20 concept
std::suspend_always yield_value(From&& from)
{
value_ = std::forward<From>(from); // caching the result in promise
return {};
}
void return_void() {}
};
handle_type h_;
Generator(handle_type h) : h_(h) {}
~Generator() { h_.destroy(); }
explicit operator bool()
{
fill(); // The only way to reliably find out whether or not we finished coroutine,
// whether or not there is going to be a next value generated (co_yield)
// in coroutine via C++ getter (operator () below) is to execute/resume
// coroutine until the next co_yield point (or let it fall off end).
// Then we store/cache result in promise to allow getter (operator() below
// to grab it without executing coroutine).
//唯一可靠地查明我们是否完成了协程的方法,
//是否会生成下一个值(co_yield)
//在协程序中,通过C++ getter(下面的operator())执行/恢复
//协程,直到下一个co_yield点(或者让它落下)。
//然后,我们将结果存储/缓存在promise中,以允许下面的getter(operator())
//在不执行协程的情况下获取它)。
return !h_.done();
}
T operator()()
{
fill();
full_ = false; // we are going to move out previously cached
// result to make promise empty again
return std::move(h_.promise().value_);
}
private:
bool full_ = false;
void fill()
{
if (!full_)
{
h_();
if (h_.promise().exception_)
std::rethrow_exception(h_.promise().exception_);
// propagate coroutine exception in called context
// 在调用的上下文中传播协同程序异常
full_ = true;
}
}
};
Generator<std::uint64_t>
fibonacci_sequence(unsigned n)
{
if (n == 0)
co_return;
if (n > 94)
throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow.");
co_yield 0;
if (n == 1)
co_return;
co_yield 1;
if (n == 2)
co_return;
std::uint64_t a = 0;
std::uint64_t b = 1;
for (unsigned i = 2; i < n; ++i)
{
std::uint64_t s = a + b;
co_yield s;
a = b;
b = s;
}
}
int main()
{
try
{
auto gen = fibonacci_sequence(10); // max 94 before uint64_t overflows
for (int j = 0; gen; ++j)
std::cout << "fib(" << j << ")=" << gen() << '\n';
}
catch (const std::exception& ex)
{
std::cerr << "Exception: " << ex.what() << '\n';
}
catch (...)
{
std::cerr << "Unknown exception.\n";
}
}
输出结果:
fib(0)=0
fib(1)=1
fib(2)=1
fib(3)=2
fib(4)=3
fib(5)=5
fib(6)=8
fib(7)=13
fib(8)=21
fib(9)=34