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://www.pswp.cn/news/914554.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914554.shtml
英文地址,请注明出处:http://en.pswp.cn/news/914554.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI大模型训练的云原生实践:如何用Kubernetes指挥千卡集群?

当你的团队还在手动拼装显卡集群时&#xff0c;聪明人早已教会Kubernetes自动调度千卡。就像交响乐团需要指挥家&#xff0c;万级GPU需要云原生调度艺术。深夜的机房&#xff0c;硬件工程师老张盯着监控屏上跳动的红色警报——手工组装的千卡集群再次因单点故障崩溃。而隔壁团队…

java 在k8s中的部署流程

1.写Docker文件FROM ubuntu:22.04ENV LANGC.UTF-8 LC_ALLC.UTF-8RUN apt-get update \&& DEBIAN_FRONTENDnoninteractive apt-get install -y --no-install-recommends tzdata curl ca-certificates fontconfig locales binutils \&& echo "C.UTF-8 UTF-8…

静电式 vs UV 光解:哪种油烟净化技术更适合你的餐厅?

在餐饮行业&#xff0c;油烟净化是维持厨房环境、保障周边空气质量的关键环节。静电式与 UV 光解作为两种主流净化技术&#xff0c;各有其适用范围与局限性。选择时需结合餐厅的烹饪类型、油烟特点及环保要求&#xff0c;而非盲目追求技术先进或价格高低。一、技术原理&#xf…

Java全栈工程师面试实录:从电商系统到AIGC的层层递进

场景&#xff1a;互联网大厂Java面试官 vs 搞笑程序员小曾 第一轮提问 面试官&#xff1a;小曾&#xff0c;我们公司正在重构一个高并发的电商系统&#xff0c;需要使用Spring Cloud Alibaba进行服务拆分。你能描述一下如何用Nacos进行服务注册与发现&#xff0c;并解决服务雪崩…

C++ CRTP

C CRTP&#xff08;奇异递归模板模式&#xff09;CRTP 是什么&#xff1f; 一句话总结&#xff1a;CRTP 就是让子类把自己作为模板参数传递给父类。 听起来有点绕&#xff0c;直接上代码就明白了&#xff1a; template <typename Derived> class Base {// ... };class De…

21.映射字典的值

有时候你会希望保留字典的键不变,但将每个键对应的值应用一个函数进行转换,比如提取字段、做数学运算、格式化等。 ✅ 基本用法 你可以使用 dict.items() 搭配字典推导式或生成器表达式来实现。 def map_values(obj, fn):return dict((k, fn(v)

【算法】贪心算法:摆动序列C++

文章目录前言题目解析算法原理代码示例策略证明前言 题目的链接&#xff0c;大家可以先试着去做一下再来看一下思路。376. 摆动序列 - 力扣&#xff08;LeetCode&#xff09; 题目解析 将题目有用的信息划出来&#xff0c;结合示例认真阅读&#xff0c;去理解题目。 我们的摆…

【DOCKER】-6 docker的资源限制与监控

文章目录1、docker的资源限制1.1 容器资源限制的介绍1.2 OOM1.3 容器的内存限制1.3.1 内存限制的相关选项1.4 容器的CPU限制介绍2、docker的监控插件2.1 cadvisor2.2 portainer1、docker的资源限制 1.1 容器资源限制的介绍 默认情况下&#xff0c;容器没有资源的使用限制&…

gcc 源码分析--gimple 关键数据结构

gimple 操作码&#xff0c;支持这些&#xff1a;DEFGSCODE(GIMPLE_symbol, printable name, GSS_symbol). */ DEFGSCODE(GIMPLE_ERROR_MARK, "gimple_error_mark", GSS_BASE) DEFGSCODE(GIMPLE_COND, "gimple_cond", GSS_WITH_OPS) DEFGSCODE(GIMPLE_DEBU…

TDengine GREATEST 和 LEAST 函数用户手册

TDengine GREATEST 和 LEAST 函数用户手册 1. 需求背景 1.1 问题描述 在实际生产过程中&#xff0c;客户经常需要计算三相电流、电压的最大值和最小值。传统的实现方式需要使用复杂的 CASE WHEN 语句&#xff0c;例如&#xff1a; -- 传统方式&#xff1a;计算三相电流最大…

Redis 与数据库不一致问题及解决方案

一、不一致的原因分析 1. 缓存更新策略不当 先更新数据库后删除缓存:删除缓存失败会导致不一致 先删除缓存后更新数据库:并发请求可能导致不一致 缓存穿透:大量请求直接打到数据库,绕过缓存 2. 并发操作问题 读写并发:读请求获取旧缓存时,写请求更新了数据库但未更新缓存…

iOS 加固工具使用经验与 App 安全交付流程的实战分享

在实际开发中&#xff0c;iOS App不仅要安全&#xff0c;还要能被稳定、快速、无误地交付。这在外包、B端项目、渠道分发、企业自用系统等场景中尤为常见。 然而&#xff0c;许多开发者在引入加固工具后会遇到以下困扰&#xff1a; 混淆后App运行异常、不稳定&#xff1b;资源路…

Windows 下 Visual Studio 开发 C++ 项目的部署流程

在Windows环境中使用Visual Studio&#xff08;以下简称VS&#xff09;开发C项目时&#xff0c;“部署”是确保程序能在目标设备上正常运行的关键环节。部署的核心目标是&#xff1a;将编译生成的可执行文件&#xff08;.exe&#xff09;、依赖的动态链接库&#xff08;.dll&am…

yolo8+声纹识别(实时字幕)

现在已经完成了人脸识别跟踪 ✅&#xff0c;接下来要&#xff1a; ✅ 加入「声纹识别&#xff08;说话人识别&#xff09;」功能&#xff0c;识别谁在讲话&#xff0c;并在视频中“这个人”的名字旁边加上「正在讲话」。 这属于多模态识别&#xff08;视觉 音频&#xff09;&a…

DH(Denavit–Hartenberg)矩阵

DH 矩阵&#xff08;Denavit-Hartenberg 矩阵&#xff09;是 1955 年由 Denavit 和 Hartenberg 提出的一种机器人运动学建模方法&#xff0c;用于描述机器人连杆和关节之间的关系。该方法通过在机器人每个连杆上建立坐标系&#xff0c;并用 44 的齐次变换矩阵&#xff08;DH 矩…

Vim的magic模式

在 Vim 中&#xff0c;magic 模式用于控制正则表达式中特殊字符的解析方式。它决定了哪些字符需要转义才能发挥特殊作用&#xff0c;从而影响搜索和替换命令的写法。以下是详细介绍&#xff1a; 一、三种 magic 模式 Vim 提供三种 magic 模式&#xff0c;通过在正则表达式前添加…

Git 使用技巧与原理(一)—— 基础操作

1、起步 1.1 版本控制 版本控制是一种记录一个或若干文件内容变化&#xff0c;以便将来查阅特定版本修订情况的系统。 版本控制系统&#xff08;VCS&#xff0c;Version Control System&#xff09;通常可以分为三类&#xff1a; 本地版本控制系统&#xff1a;大多都是采用某…

软件测试之自动化测试

目录 1.什么是自动化测试 2.web⾃动化测试 2.1驱动 WebDriverManager 3. Selenium 3.1selenium驱动浏览器的⼯作原理 4.常用函数 4.1元素的定位 4.1.1cssSelector选择器 4.2.2xpath 4.2操作测试对象 4.3窗⼝ 4.4等待 4.5浏览器导航 4.6弹窗 4.7文件上传 4.8设置…

sqlserver迁移日志文件和数据文件

sqlserver安装后没有指定日志存储路径或者还原库指定的日志存储位置不理想想要更改&#xff0c;都可以按照这种方式来更换&#xff1b;1.前提准备&#xff1a;数据库的备份bak文件2.查看自己当前数据库的日志文件和数据文件存储路径是否理想选中当前数据库&#xff0c;右键属性…

MFC UI表格制作从专家到入门

文章目录CListCtrl常见问题增强版CGridCtrl&#xff08;第三方&#xff09;第三方库ReoGridCListCtrl 默认情况下&#xff0c;CListCtrl不支持直接编辑单元格&#xff0c;需通过消息处理实现。 1.添加控件到资源视图 在对话框资源编辑器中拖入List Control控件&#xff0c;设…