当前位置: 首页 > news >正文

C++20 协程参考手册详解 - 源自 cppreference.com

C++20 协程参考手册详解 - 源自 cppreference.com

人话版

先说“人说”,简化版本,更易理解。

宏观概念协程是一个可以暂定和恢复执行的函数。(普通函数是线程相关的,函数的调用依赖于线程栈,而协程的运行状态暂定状态保存在堆区,协程在线程A暂停后,后续可以在另一个线程B继续上次的状态执行。)

具体的:在C++中,只要函数出现了co_awaitco_yieldco_return这三个关键字之一,函数就由编译器变为协程。

co_await

co_await的作用是让协程“暂停一下”,等待某个操作(比如网络请求或文件读取)完成后,再继续执行。co_await就是这个“等一等”的动作,暂停协程,干别的事,等条件满足再回来。

但问题来了:如果你直接对一个自定义类型用co_await,比如co_await IntReader{},编译器会一脸懵逼。它不知道这个类型啥时候算“完成”,也不知道结果在哪儿。为了让co_await能用,我们需要让自定义类型遵守一个规则,这个规则叫 Awaitable

Awaitable就像一份“协程使用说明书”,告诉编译器怎么处理暂停和恢复。它要求你的类型实现三个关键函数:

  • bool await_ready():告诉协程“现在能不能直接执行?”
    • 返回类型:bool
    • 作用:在执行co_awit时,先执行这个函数检测是否可以立即执行,避免不必要的暂停。
  • void await_suspend(std::coroutine_handle<> h):如果要暂停,接下来该干啥?
    • 返回类型:可以是void;也可以是boolture表暂停,false表不暂停。
    • 参数:coroutine_handle<> 协程的“遥控器”。本质上,std::coroutine_handle 是一个指向堆上分配的协程帧(coroutine frame)的、轻量级的指针。std::coroutine_handle<> (通用遥控器),它可以指向任何类型的协程,无论其 promise_type 是什么,因为它不需要关心挂起的是哪种协程,它只需要一个通用的句柄,以便之后能调用 .resume() 即可。coroutine_handle<PromiseType> (专用遥控器)因为编译器知道 Promise 的具体类型,所以你可以安全地调用 .promise() 方法,获得对 PromiseType 对象的引用,然后从中取出结果。
    • 作用:如果协程要暂停会调用await_suspend(),通过参数拿到协程句柄(指向当前暂定的协程实例),可以在未来某个时刻“唤醒”协程(h.resume())。
  • void await_resume():恢复时,返回什么结果?
    • 返回类型:可以是void,也可以是具体类型
    • 作用:当协程恢复执行时(或压根没暂停)被调用,其返回值就是co_awit表达式的结果。
  • 这三个函数一起合作,让co_await知道如何暂停、等待和继续

initial_suspendfinal_suspend相当于协程生命周期开始和结束时的两个“守门人”,它们通过返回一个 Awaitable 对象(通常是 std::suspend_alwaysstd::suspend_never)来决定协程在两个关键时刻的行为:

  • initial_suspend:执行您编写的任何一行协程体代码之前,initial_suspend 会被自动调用并 co_await,它回答了一个关键问题:“协程被调用后**,**是应该立刻开始执行,还是应该创建一个‘暂停’的任务,等待调用者明确命令才开始?”
  • final_suspend:当协程的函数体执行完毕(无论是通过 co_return 正常结束,还是执行到函数末尾隐式结束)并且所有局部变量都已被析构之后,final_suspend 会被自动调用并 co_await。它回答了另一个关键问题:“协程执行完毕后,是应该立即自我销毁,还是应该停留在‘已完成’的状态,等待外界来处理它的‘后事’(比如读取返回值)?”
#include <iostream>
#include <coroutine>
#include <thread>
#include <unistd.h>class IntReader{
public:bool await_ready() { return false;}void await_suspend(std::coroutine_handle<> handle){std::thread thread([this,handle](){sleep(1);value_ = 1;handle.resume();});thread.detach();}int await_resume(){return value_;}
private:int value_;};// Task这里先不用管,只需要关注上面的IntReader类
class Task;
class Task{
public:class promise_type{public:Task get_return_object() { return {}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void unhandled_exception() {}void return_void() {}};
};Task PrintInt(){IntReader reader1;int total = co_await reader1;IntReader reader2;total += co_await reader2;IntReader reader3;total += co_await reader3;std::cout<<total<<std::endl;
}
int main(){PrintInt();getchar();return 0;
}

co_return

协程的返回类型要求

C++对协程的返回类型只有一个硬性规定:它必须包含一个名为promise_type的内嵌类型

当你调用一个协程函数时:

  • 编译器会在堆上分配空间,保存协程的状态
  • 同时创建一个promise_type对象,嵌在返回类型里
  • 通过promise_type定义的函数,你可以控制协程的行为,或者与调用者交换数据。

promise_type的核心函数:get_return_object()

promise_type必须实现一个函数:get_return_object()。它的作用是创建协程的返回值对象。

调用时机:

  • 在协程函数被调用时,编译器会先创建promise_typer对象,然后调用get_return_object(),生成返回类型(比如Task)给调用者。
  • promise_type是返回类型的内嵌类型,但编译器不会直接构造返回类型,而是通过promise_type来“间接”生成它。

返回值的作用:

  • 取决于设计者的意图。如果只是想让协程干活(比如打印),返回值可以是个空壳。
  • 如果想让协程返回数据, 就要在返回类型里设计获取数据的接口。
#include <iostream>
#include <coroutine>
#include <memory>
#include <thread>
#include <unistd.h>class IntReader{
public:bool await_ready() { return false;}void await_suspend(std::coroutine_handle<> handle){std::thread thread([this,handle](){sleep(1);value_ = 1;handle.resume();});thread.detach();}int await_resume(){return value_;}
private:int value_;};class Task;
class Task{
public:class promise_type{public:promise_type(): value_(std::make_shared<int>()) {}Task get_return_object() { return Task{value_}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void unhandled_exception() {}void return_value(int value){*value_=value;}private:std::shared_ptr<int> value_;};Task(const std::shared_ptr<int>& value): value_(value) {}int GetValue() const{return *value_;}}
private:std::shared_ptr<int> value_;};Task GetInt(){IntReader reader1;int total = co_await reader1;IntReader reader2;total += co_await reader2;IntReader reader3;total += co_await reader3;co_return total;
}
int main(){auto task = GetInt();sleep(4);	// 这里为了方便,直接等4秒std::cout<<task.GetValue()<<std::endl;getchar();return 0;
}

co_yield

co_yield 表达式向调用者返回一个值并暂停当前协程:它是可恢复生成器 (generator) 函数的常见构建块。

co_yield expr
co_yield braced-init-list

它等价于:

co_await promise.yield_value(expr)

一个典型的生成器的 yield_value 会将其参数存储(复制/移动或仅存储地址,因为参数的生命周期在 co_await 内部跨越了暂停点)到生成器对象中,并返回 std::suspend_always,将控制权转移给调用者/恢复者。

示例:斐波那契生成器
#include <coroutine>
#include <cstdint>
#include <exception>
#include <iostream>template<typename T>
struct Generator {// 类名 'Generator' 是我们的选择,对于协程魔法来说不是必需的。// 编译器通过 'co_yield' 关键字的存在来识别协程。// 你可以使用 'MyGenerator' (或任何其他名字),只要你包含// 一个带有 'MyGenerator get_return_object()' 方法的嵌套结构体 promise_type。// (注意:重命名时需要调整构造函数和析构函数的声明。)struct promise_type;using handle_type = std::coroutine_handle<promise_type>;struct promise_type // 必需{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(); } // 保存异常template<std::convertible_to<T> From> // C++20 conceptstd::suspend_always yield_value(From&& from) {value_ = std::forward<From>(from); // 在 promise 中缓存结果return {};}void return_void() {}};handle_type h_;Generator(handle_type h) : h_(h) {}~Generator() { h_.destroy(); }explicit operator bool() {fill(); // 要可靠地判断协程是否结束,// 以及是否将通过C++的getter(下方的operator())在协程中生成下一个值(co_yield),// 唯一的方法是执行/恢复协程直到下一个co_yield点(或让它执行完毕)。// 然后我们将结果存储/缓存在 promise 中,以允许 getter (下方的operator())// 在不执行协程的情况下获取它。return !h_.done();}T operator()() {fill();full_ = false; // 我们将要移出先前缓存的结果,使 promise 再次变空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_);// 在调用上下文中传播协程异常full_ = true;}}
};Generator<std::uint64_t>
fibonacci_sequence(unsigned n) {if (n == 0)co_return;if (n > 94)throw std::runtime_error("斐波那契序列太大,元素会溢出。");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); // 在 uint64_t 溢出前最大为 94for (int j = 0; gen; ++j)std::cout << "fib(" << j << ")=" << gen() << '\n';}catch (const std::exception& ex) {std::cerr << "异常: " << ex.what() << '\n';}catch (...) {std::cerr << "未知异常。\n";}
}

执行流程

每个协程都与以下三者相关联:

  1. Promise 对象 (the promise object):在协程内部被操纵。协程通过此对象提交其结果或异常。Promise 对象与 std::promise 没有任何关系。
  2. 协程句柄 (the coroutine handle):在协程外部被操纵。这是一个**非拥有式(non-owning)**的句柄,用于恢复协程的执行或销毁协程帧。
  3. 协程状态 (the coroutine state):这是一个内部的、动态分配(除非分配被优化掉)的对象,它包含:
    • Promise 对象。
    • 所有参数(均按值复制)。
    • 当前暂停点的某种表示,以便恢复时知道从何处继续,销毁时知道哪些局部变量在作用域内。
    • 生命周期跨越当前暂停点的局部变量和临时对象。

当一个协程开始执行时,它会执行以下操作:

  1. 使用 operator new 分配协程状态对象
  2. 将所有函数参数复制到协程状态中:按值传递的参数被移动或复制,按引用传递的参数仍然是引用(因此,如果协程在被引用对象的生命周期结束后恢复,可能会导致悬挂引用——见下文示例)。
  3. 调用 Promise 对象的构造函数。如果 Promise 类型有一个接受所有协程参数的构造函数,则会使用该构造函数并传入(复制后的)协程参数。否则,调用默认构造函数。
  4. 调用 promise.get_return_object() 并将结果保存在一个局部变量中。该调用的结果将在协程首次暂停时返回给调用者。在此步骤及之前抛出的任何异常都会传播回调用者,而不会被放入 Promise 中。
  5. 调用 promise.initial_suspend()co_await 其结果。典型的 Promise 类型会返回 std::suspend_always(用于懒启动的协程)或 std::suspend_never(用于急切启动的协程)。
  6. co_await promise.initial_suspend() 恢复时,开始执行协程的主体部分。

当协程到达一个暂停点时:

  • 先前获得的返回对象会被返回给调用者/恢复者(如有必要,会进行到协程返回类型的隐式转换)。

当协程到达 co_return 语句时,它会执行以下操作:

  1. 对于 co_return;co_return expr;(其中 expr 类型为 void),调用 promise.return_void()
  2. 对于 co_return expr;(其中 expr 类型非 void),调用 promise.return_value(expr)
  3. 按创建顺序的逆序,销毁所有具有自动存储期的变量。
  4. 调用 promise.final_suspend()co_await 其结果。

如果协程执行完函数体末尾而没有 co_return,这等价于 co_return;,但如果 Promise 域中找不到 return_void 的声明,则行为是未定义的。一个在其函数体内没有任何协程定义关键字的函数不是协程,无论其返回类型如何,如果其返回类型不是 void(可有 cv 限定),则执行到函数末尾会导致未定义行为。

如果协程因未捕获的异常而结束,它会执行以下操作:

  1. 捕获异常并在 catch 块内调用 promise.unhandled_exception()
  2. 调用 promise.final_suspend()co_await 其结果(例如,用于恢复一个延续或发布一个结果)。从此时恢复协程是未定义行为。

当协程状态被销毁时(无论是通过 co_return、未捕获的异常终止,还是通过其句柄销毁),它会执行以下操作:

  1. 调用 Promise 对象的析构函数。
  2. 调用函数参数副本的析构函数。
  3. 调用 operator delete 来释放协程状态所使用的内存。
  4. 将执行权交还给调用者/恢复者。

以上“人话版”是看完视频后的总结于扩展。


官方手册

引言

本篇内容是 en.cppreference.com Coroutines (C++20) 页面的完整中文翻译,旨在为需要精确参考的 C++ 开发者提供一份详尽的中文对应文档。


协程 (C++20)

协程是一种可以暂停执行以便后续恢复的函数。协程是无栈的(stackless):它们通过返回至调用者来暂停执行,而恢复执行所需的数据与栈分离存储。这使得顺序执行的代码可以异步执行(例如,无需显式回调即可处理非阻塞 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)

  • 协程不能使用可变参数(variadic arguments)、普通的 return 语句或占位符返回类型autoConcept)。
  • consteval 函数、constexpr 函数、构造函数、析构函数以及 main 函数不能是协程。

执行 (Execution)

每个协程都与以下三者相关联:

  1. Promise 对象 (the promise object):在协程内部被操纵。协程通过此对象提交其结果或异常。Promise 对象与 std::promise 没有任何关系。
  2. 协程句柄 (the coroutine handle):在协程外部被操纵。这是一个**非拥有式(non-owning)**的句柄,用于恢复协程的执行或销毁协程帧。
  3. 协程状态 (the coroutine state):这是一个内部的、动态分配(除非分配被优化掉)的对象,它包含:
    • Promise 对象。
    • 所有参数(均按值复制)。
    • 当前暂停点的某种表示,以便恢复时知道从何处继续,销毁时知道哪些局部变量在作用域内。
    • 生命周期跨越当前暂停点的局部变量和临时对象。

当一个协程开始执行时,它会执行以下操作:

  1. 使用 operator new 分配协程状态对象
  2. 将所有函数参数复制到协程状态中:按值传递的参数被移动或复制,按引用传递的参数仍然是引用(因此,如果协程在被引用对象的生命周期结束后恢复,可能会导致悬挂引用——见下文示例)。
  3. 调用 Promise 对象的构造函数。如果 Promise 类型有一个接受所有协程参数的构造函数,则会使用该构造函数并传入(复制后的)协程参数。否则,调用默认构造函数。
  4. 调用 promise.get_return_object() 并将结果保存在一个局部变量中。该调用的结果将在协程首次暂停时返回给调用者。在此步骤及之前抛出的任何异常都会传播回调用者,而不会被放入 Promise 中。
  5. 调用 promise.initial_suspend()co_await 其结果。典型的 Promise 类型会返回 std::suspend_always(用于懒启动的协程)或 std::suspend_never(用于急切启动的协程)。
  6. 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} 已销毁h.resume(); // 恢复的协程执行 std::cout << i,在 S::i 释放后使用h.destroy();
}coroutine bad2() {S s{0};return s.f(); // 返回的协程无法在不产生“使用已释放内存”的情况下被恢复
}void bad3() {coroutine h = [i = 0]() -> coroutine // 一个同时也是协程的 lambda{std::cout << i;co_return;}(); // 立即调用// lambda 已销毁h.resume(); // 在 (匿名 lambda 类型)::i 释放后使用h.destroy();
}void good() {coroutine h = [](int i) -> coroutine // 将 i 设为协程参数{std::cout << i;co_return;}(0);// lambda 已销毁h.resume(); // 没有问题,i 作为一个按值传递的参数已被复制到协程帧中h.destroy();
}

当协程到达一个暂停点时:

  • 先前获得的返回对象会被返回给调用者/恢复者(如有必要,会进行到协程返回类型的隐式转换)。

当协程到达 co_return 语句时,它会执行以下操作:

  1. 对于 co_return;co_return expr;(其中 expr 类型为 void),调用 promise.return_void()
  2. 对于 co_return expr;(其中 expr 类型非 void),调用 promise.return_value(expr)
  3. 按创建顺序的逆序,销毁所有具有自动存储期的变量。
  4. 调用 promise.final_suspend()co_await 其结果。

如果协程执行完函数体末尾而没有 co_return,这等价于 co_return;,但如果 Promise 域中找不到 return_void 的声明,则行为是未定义的。一个在其函数体内没有任何协程定义关键字的函数不是协程,无论其返回类型如何,如果其返回类型不是 void(可有 cv 限定),则执行到函数末尾会导致未定义行为。

// 假设 task 是某种协程任务类型
task<void> f() {// 不是协程,未定义行为
}task<void> g() {co_return;  // OK
}task<void> h() {co_await g();// OK,隐式的 co_return;
}

如果协程因未捕获的异常而结束,它会执行以下操作:

  1. 捕获异常并在 catch 块内调用 promise.unhandled_exception()
  2. 调用 promise.final_suspend()co_await 其结果(例如,用于恢复一个延续或发布一个结果)。从此时恢复协程是未定义行为。

当协程状态被销毁时(无论是通过 co_return、未捕获的异常终止,还是通过其句柄销毁),它会执行以下操作:

  1. 调用 Promise 对象的析构函数。
  2. 调用函数参数副本的析构函数。
  3. 调用 operator delete 来释放协程状态所使用的内存。
  4. 将执行权交还给调用者/恢复者。

动态分配 (Dynamic allocation)

协程状态通过非数组形式的 operator new 进行动态分配。

  • 如果 Promise 类型定义了类级别的替换,则会使用它;否则,使用全局的 operator new

  • 如果 Promise 类型定义了接受额外参数的放置式 operator new,并且这些参数与一个参数列表匹配(第一个参数是请求的大小 std::size_t,其余是协程函数的参数),那么这些参数将被传递给 operator new(这使得对协程使用前置分配器约定 (leading-allocator-convention) 成为可能)。

  • 如果以下条件满足,对 operator new 的调用可以被优化掉(即使使用了自定义分配器):

    1. 协程状态的生命周期严格嵌套在调用者的生命周期内,并且
    2. 协程帧的大小在调用点已知。
  • 在这种情况下,协程状态被嵌入在调用者的栈帧(如果调用者是普通函数)或协程状态(如果调用者是协程)中。

  • 如果分配失败,协程会抛出 std::bad_alloc,除非Promise 类型定义了成员函数Promise::get_return_object_on_allocation_failure()。如果定义了该函数,分配将使用 nothrow 形式的 operator new,并且在分配失败时,协程会立即将从 Promise::get_return_object_on_allocation_failure() 获得的对象返回给调用者。例如:

    struct Coroutine::promise_type {/* ... */// 确保使用不会抛出异常的 operator-newstatic Coroutine get_return_object_on_allocation_failure() {std::cerr << __func__ << '\n';throw std::bad_alloc(); // 或者, return Coroutine(nullptr);}// 自定义的非抛出异常的 new 重载void* operator new(std::size_t n) noexcept {if (void* mem = std::malloc(n))return mem;return nullptr; // 分配失败}
    };
    

Promise

Promise 类型由编译器根据协程的返回类型,使用 std::coroutine_traits 来确定。

正式地说,令 RArgs... 分别表示协程的返回类型和参数类型列表,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,如果协程是右值引用限定的隐式对象成员函数。

例如:

如果协程定义为…那么其 Promise 类型是…
task<void> foo(int x);std::coroutine_traits<task<void>, int>::promise_type
task<void> Bar::foo(int x) const;std::coroutine_traits<task<void>, const Bar&, int>::promise_type
task<void> Bar::foo(int x) &&;std::coroutine_traits<task<void>, Bar&&, int>::promise_type

好的,这是您指定的 cppreference.com 协程页面后续内容的完整、忠实翻译。


co_await

一元运算符 co_await 暂停协程并将控制权返回给调用者。

co_await expr

co_await 表达式只能出现在常规函数体(包括 lambda 表达式的函数体)内的潜在求值表达式 (potentially-evaluated expression) 中,且不能出现在:

  • handler 中。
  • declaration 语句中,除非它出现在该声明的初始化器中。
  • init-statementsimple-declaration 中(参见 ifswitchforrange-for),除非它出现在该 init-statement 的初始化器中。
  • 默认参数中。
  • 具有静态或线程存储期的块作用域变量的初始化器中。
  • (C++26 起) co_await 表达式不能是契约断言 (contract assertion) 谓词的潜在求值子表达式。

首先,expr 被转换为一个可等待对象 (awaitable),如下所示:

  1. 如果 expr 来自初始暂停点、最终暂停点或 yield 表达式,则 awaitable 就是 expr 本身。
  2. 否则,如果当前协程的 Promise 类型有成员函数 await_transform,则 awaitable 是 promise.await_transform(expr) 的结果。
  3. 否则,awaitable 就是 expr 本身。

然后,获得 awaiter 对象,如下所示:

  1. 如果 operator co_await 的重载决议给出了唯一的最佳重载,则 awaiter 是该调用的结果:
    • 对于成员重载:awaitable.operator co_await()
    • 对于非成员重载:operator co_await(static_cast<Awaitable&&>(awaitable))
  2. 否则,如果没有找到 operator co_await,则 awaiter 就是 awaitable 本身。
  3. 否则,如果重载决议有歧义,则程序是病态的 (ill-formed)。

如果上述表达式是一个右值(prvalue),则 awaiter 对象是从它物化(materialized)出的一个临时对象。否则,如果表达式是一个泛左值(glvalue),则 awaiter 对象是它所引用的对象。

接着,调用 awaiter.await_ready() (这是一个快捷方式,用于在已知结果已就绪或可同步完成时避免挂起的开销)。如果其结果(在上下文中转换为 bool 后)为 false,则:

  1. 协程被暂停(其协程状态被填充了局部变量和当前暂停点)。
  2. 调用 awaiter.await_suspend(handle),其中 handle 是表示当前协程的句柄。在此函数内部,被挂起的协程状态可通过该句柄观察到,并且此函数有责任调度它在某个执行器上恢复,或被销毁(返回 false 算作一种调度)。
    • 如果 await_suspend 返回 void,控制权立即返回给当前协程的调用者/恢复者(此协程保持挂起状态)。
    • 否则,如果 await_suspend 返回 bool
      • true 将控制权返回给当前协程的调用者/恢复者。
      • false 则恢复当前协程的执行。
    • 如果 await_suspend 返回某个其他协程的句柄,则该句柄被恢复(通过调用 handle.resume())(注意,这可能形成调用链,并最终导致当前协程被恢复)。
    • 如果 await_suspend 抛出异常,异常被捕获,协程被恢复,然后异常被立即重新抛出。

最后,awaiter.await_resume() 被调用(无论协程是否被挂起),其结果就是整个 co_await expr 表达式的结果。

如果协程在 co_await 表达式中被挂起,并在稍后被恢复,则恢复点位于调用 awaiter.await_resume() 之前。

注意:协程在进入 awaiter.await_suspend() 之前已完全挂起。它的句柄可以被共享给另一个线程,并在 await_suspend() 函数返回之前被恢复。(注意,默认的内存安全规则仍然适用,因此如果协程句柄在没有锁的情况下跨线程共享,awaiter 应该至少使用释放语义 (release semantics),而恢复者应该至少使用获取语义 (acquire semantics)。)例如,协程句柄可以放入一个回调中,在异步 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(); });// 潜在的未定义行为: 访问可能已被销毁的 *this// std::cout << "New thread ID: " << p_out->get_id() << '\n';std::cout << "New thread ID: " << out.get_id() << '\n'; // 这样是 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 在此处被销毁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_alwaysstd::suspend_never


好的,遵照您的指示,这是对您提供的剩余部分内容的完整、忠实的中文翻译和博客形式的重写。


示例:promise_type::await_transform 和程序提供的 Awaiter

这个例子演示了 promise_type 如何通过 await_transform 成员函数来“拦截”并转换一个 co_await 表达式,返回一个自定义的 awaiter,从而实现对协程暂停行为的动态控制。

#include <cassert>
#include <coroutine>
#include <iostream>struct tunable_coro {// 一个 Awaiter,其“就绪”状态由构造函数的参数决定。class tunable_awaiter {bool ready_;public:explicit(false) tunable_awaiter(bool ready) : ready_{ready} {}// 三个标准的 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(); }// 一个用户提供的转换函数,它返回自定义的 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); }// 为简化起见,将这4个特殊成员函数声明为 deleted: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 << ' ';// 传递给 co_await 的 awaiter 会进入 promise_type::await_transform,// 它会发出一个 tunable_awaiter,这个 awaiter 最初会导致挂起(在每次迭代时返回到 main),// 但在调用 disable_suspension 之后,就不会再发生挂起,// 循环会一直运行到结束而不会返回到 main()。co_await std::suspend_always{};}
}int main() {auto coro = generate(8);coro(); // 只会发出第一个元素 == 0for (int k{}; k < 4; ++k) {coro(); // 每次迭代发出 1 2 3 4 中的一个std::cout << ": ";}coro.disable_suspension();coro(); // 一次性发出剩余的数字 5 6 7
}

输出:

0 1 : 2 : 3 : 4 : 5 6 7 

co_yield

co_yield 表达式向调用者返回一个值并暂停当前协程:它是可恢复生成器 (generator) 函数的常见构建块。

co_yield expr
co_yield braced-init-list

它等价于:

co_await promise.yield_value(expr)

一个典型的生成器的 yield_value 会将其参数存储(复制/移动或仅存储地址,因为参数的生命周期在 co_await 内部跨越了暂停点)到生成器对象中,并返回 std::suspend_always,将控制权转移给调用者/恢复者。

示例:斐波那契生成器
#include <coroutine>
#include <cstdint>
#include <exception>
#include <iostream>template<typename T>
struct Generator {// 类名 'Generator' 是我们的选择,对于协程魔法来说不是必需的。// 编译器通过 'co_yield' 关键字的存在来识别协程。// 你可以使用 'MyGenerator' (或任何其他名字),只要你包含// 一个带有 'MyGenerator get_return_object()' 方法的嵌套结构体 promise_type。// (注意:重命名时需要调整构造函数和析构函数的声明。)struct promise_type;using handle_type = std::coroutine_handle<promise_type>;struct promise_type // 必需{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(); } // 保存异常template<std::convertible_to<T> From> // C++20 conceptstd::suspend_always yield_value(From&& from) {value_ = std::forward<From>(from); // 在 promise 中缓存结果return {};}void return_void() {}};handle_type h_;Generator(handle_type h) : h_(h) {}~Generator() { h_.destroy(); }explicit operator bool() {fill(); // 要可靠地判断协程是否结束,// 以及是否将通过C++的getter(下方的operator())在协程中生成下一个值(co_yield),// 唯一的方法是执行/恢复协程直到下一个co_yield点(或让它执行完毕)。// 然后我们将结果存储/缓存在 promise 中,以允许 getter (下方的operator())// 在不执行协程的情况下获取它。return !h_.done();}T operator()() {fill();full_ = false; // 我们将要移出先前缓存的结果,使 promise 再次变空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_);// 在调用上下文中传播协程异常full_ = true;}}
};Generator<std::uint64_t>
fibonacci_sequence(unsigned n) {if (n == 0)co_return;if (n > 94)throw std::runtime_error("斐波那契序列太大,元素会溢出。");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); // 在 uint64_t 溢出前最大为 94for (int j = 0; gen; ++j)std::cout << "fib(" << j << ")=" << gen() << '\n';}catch (const std::exception& ex) {std::cerr << "异常: " << ex.what() << '\n';}catch (...) {std::cerr << "未知异常。\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

注解 (Notes)

特性测试宏
特性标准
Coroutines (编译器支持)__cpp_impl_coroutine201902L(C++20)
Coroutines (库支持)__cpp_lib_coroutine201902L(C++20)
std::generator__cpp_lib_generator202207L(C++23)
关键字

co_await, co_return, co_yield

库支持

协程支持库定义了几个类型,为协程提供编译时和运行时的支持。

缺陷报告 (Defect reports)

以下行为变更的缺陷报告被追溯应用于先前发布的C++标准。

DR应用于发布时的行为正确的行为
CWG 2556C++20无效的 return_void 导致函数末尾结束的行为未定义在此情况下程序是病态的 (ill-formed)
CWG 2668C++20co_await 不能出现在 lambda 表达式中允许
CWG 2754C++23为显式对象成员函数构造 promise 对象时会捕获 *this在此情况下不捕获 *this

文章转载自:
http://acheb.elldm.cn
http://aurelian.elldm.cn
http://bootmaker.elldm.cn
http://cheapo.elldm.cn
http://analog.elldm.cn
http://calathos.elldm.cn
http://banefully.elldm.cn
http://autolysate.elldm.cn
http://aesthetician.elldm.cn
http://bell.elldm.cn
http://cemf.elldm.cn
http://bavarian.elldm.cn
http://burry.elldm.cn
http://caparison.elldm.cn
http://bronchiole.elldm.cn
http://bride.elldm.cn
http://banda.elldm.cn
http://capsicin.elldm.cn
http://chairlady.elldm.cn
http://babyless.elldm.cn
http://acidosis.elldm.cn
http://alienate.elldm.cn
http://aquanaut.elldm.cn
http://agronomic.elldm.cn
http://butyric.elldm.cn
http://autotruck.elldm.cn
http://astrand.elldm.cn
http://charger.elldm.cn
http://cedi.elldm.cn
http://annotation.elldm.cn
http://www.dtcms.com/a/281340.html

相关文章:

  • Expression 类的静态方法
  • PostgreSQL 大数据量(超过50GB)导出方案
  • 国产化Excel处理组件Spire.XLS教程:在 C# 中生成 Excel文件
  • 关于LM74700-Q1低IQ理想二极管的应用与参数极限
  • saltstack安装部署
  • 对象数组列表转成树形结构--树形结构转成列表(处理菜单)
  • ORA-06413: 连接未打开
  • 设计网站集:经济信息数据 统计数据 + 农业 + 金属 + 药品 + 电子 + 加密货币 + 债券 + 期货 + 其他
  • 构建企业级项目管理全面数字化运营体系︱易趋(蓝云软件)总裁唐智勇
  • 东鹏饮料牵手盈飞无限质量管理系统(QMS)
  • 多方学习与安全多方计算
  • 电动汽车制动系统及其工作原理
  • 梁的振动特征函数分析
  • 算法学习笔记(1):组合数
  • 论文 视黄素与细胞修复
  • 可下载或通过爬虫获取疾病相关数据的网站及平台,涵盖临床数据、基因关联、药品信息等方向,并附注数据特点与获取方式:(不公开)
  • PHP安全漏洞深度解析:文件包含与SSRF攻击的攻防实战
  • keeplived双击热备配置
  • chrome浏览器(chrome138.0.0.0 )无法安装扩展程序,因为它使用了不受支持的清单版本解决方案
  • GAMES101 lec2-数学基础1(线性代数)
  • 03 51单片机之独立按键控制LED状态
  • HCIA第三次综合实验:VLAN
  • 连接new服务器注意事项
  • Java核心类库深度解析与实战:从字符串处理到计算器开发
  • 外网如何连接内网中的mysql数据库服务?跨网直接访问操作,不用公网IP
  • 人机协作系列(四)AI编程的下一个范式革命——看Factory AI如何重构软件工程?
  • 力扣——1071. 字符串的最大公因子
  • 解密AWS VPC路由表:显式关联与隐式关联,谁决定了网络出口?
  • 深入剖析Java并发基石:AQS原理与实战
  • java 并发面试题2