WEBKT

C++协程:`co_await`的秘密——从原理到自定义Awaitable对象

96 0 0 0

协程基础回顾

co_await 的作用

co_await 的工作流程

Awaitable 接口

自定义 Awaitable 对象示例

总结

深入理解 Awaiter

await_ready() 的作用

await_suspend() 的作用

await_resume() 的作用

异常处理

C++ 协程的优势

总结

C++20 引入的协程(Coroutines)为异步编程带来了新的范式。co_await 关键字是协程的核心,理解它对于掌握 C++ 协程至关重要。本文将深入剖析 co_await 的工作机制,并通过自定义 awaitable 对象的示例,帮助你更好地理解和运用协程。

协程基础回顾

在深入 co_await 之前,我们先简单回顾一下协程的基本概念。

  • 协程函数 (Coroutine Function):包含 co_awaitco_yieldco_return 语句的函数。协程函数被调用时,不会立即执行,而是返回一个 coroutine object
  • Coroutine Object:一个管理协程状态的对象。它可以被启动、恢复和销毁。
  • Awaitable Object:一个可以被 co_await 的对象。它定义了协程挂起和恢复的行为。

co_await 的作用

co_await 表达式用于挂起当前协程的执行,直到 awaitable 对象准备好产生结果。它的主要作用可以概括为以下几点:

  1. 挂起 (Suspend):当 co_await 遇到一个尚未完成的 awaitable 对象时,它会挂起当前协程的执行,将控制权返回给调用者。
  2. 等待 (Wait)co_await 会等待 awaitable 对象完成,也就是 awaitable 对象的结果变得可用。
  3. 恢复 (Resume):当 awaitable 对象完成时,co_await 会恢复协程的执行,并获取 awaitable 对象的结果。

co_await 的工作流程

co_await 的工作流程可以分解为以下几个步骤:

  1. 获取 Awaitable 对象co_await 作用于一个表达式,该表达式的结果必须是一个 awaitable 对象。如果表达式的结果不是 awaitable 对象,编译器会尝试进行隐式转换。
  2. 调用 await_ready()co_await 首先调用 awaitable 对象的 await_ready() 方法。该方法返回一个 bool 值,指示 awaitable 对象是否已经准备好产生结果。如果 await_ready() 返回 true,则协程不会挂起,直接跳到第 5 步。
  3. 调用 await_suspend():如果 await_ready() 返回 false,则 co_await 调用 awaitable 对象的 await_suspend() 方法。await_suspend() 方法负责挂起协程,并将控制权返回给调用者。await_suspend() 接受一个 coroutine handle 作为参数,该 coroutine handle 可以用于在 awaitable 对象完成时恢复协程的执行。
  4. 等待 Awaitable 对象完成:在 await_suspend() 方法中,awaitable 对象会执行一些操作,例如等待 I/O 完成、等待定时器到期等。当 awaitable 对象完成时,它会使用 coroutine handle 恢复协程的执行。
  5. 调用 await_resume():当协程被恢复时,co_await 调用 awaitable 对象的 await_resume() 方法。await_resume() 方法负责返回 awaitable 对象的结果。该结果将作为 co_await 表达式的值。

Awaitable 接口

一个类型要成为 awaitable 对象,必须提供以下三个方法:

  • bool await_ready() const noexcept:检查 awaitable 对象是否已经准备好产生结果。
  • void await_suspend(std::coroutine_handle<> handle):挂起协程,并将控制权返回给调用者。handle 参数是一个 coroutine handle,用于在 awaitable 对象完成时恢复协程的执行。
  • auto await_resume():返回 awaitable 对象的结果。返回值类型可以是任意类型。

自定义 Awaitable 对象示例

为了更好地理解 co_await 的工作机制,我们来看一个自定义 awaitable 对象的示例。假设我们需要创建一个 Timer 类,该类可以在指定的时间后恢复协程的执行。

#include <iostream>
#include <chrono>
#include <thread>
#include <coroutine>
class Timer {
public:
Timer(std::chrono::milliseconds duration) : duration_(duration) {}
struct Awaiter {
Timer& timer_;
std::coroutine_handle<> handle_;
bool await_ready() const noexcept {
return false; // Always suspend
}
void await_suspend(std::coroutine_handle<> handle) noexcept {
handle_ = handle;
std::thread([this]() {
std::this_thread::sleep_for(timer_.duration_);
handle_.resume();
}).detach();
}
void await_resume() noexcept {}
};
Awaiter operator co_await() {
return {*this};
}
private:
std::chrono::milliseconds duration_;
};
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task my_coroutine() {
std::cout << "Start of coroutine" << std::endl;
co_await Timer(std::chrono::milliseconds(1000));
std::cout << "Coroutine resumed after 1 second" << std::endl;
}
int main() {
my_coroutine();
std::this_thread::sleep_for(std::chrono::milliseconds(1500)); // Keep main thread alive
return 0;
}

在这个示例中,Timer 类表示一个定时器。它接受一个时间间隔作为参数,并在指定的时间后恢复协程的执行。Timer 类定义了一个嵌套类 Awaiter,该类实现了 awaitable 接口。

  • await_ready() 方法总是返回 false,表示协程总是需要挂起。
  • await_suspend() 方法创建一个新的线程,该线程在指定的时间后恢复协程的执行。
  • await_resume() 方法不返回任何值。

Timer 类还重载了 co_await 运算符,使其返回一个 Awaiter 对象。这样,我们就可以在协程中使用 co_await Timer(std::chrono::milliseconds(1000)) 来挂起协程 1 秒钟。

代码解释:

  1. Timer: 接收一个 std::chrono::milliseconds 类型的 duration 参数,表示定时器的时间长度。
  2. Awaiter 结构体: 这是实现 awaitable 接口的关键。它包含以下成员:
    • timer_: 对 Timer 对象的引用,允许 Awaiter 访问 Timer 的 duration。
    • handle_: std::coroutine_handle<> 类型,用于在定时器到期后恢复协程。
    • await_ready(): 总是返回 false,确保协程总是被挂起。
    • await_suspend(std::coroutine_handle<> handle): 这是最重要的部分。它接收一个协程句柄 handle,然后创建一个新的线程。新线程休眠指定的时间后,调用 handle.resume() 来恢复协程的执行。detach() 方法确保线程在后台运行,不会阻塞主线程。
    • await_resume(): 这个例子中,await_resume 什么也不做。它可以用来返回一个值,但在这里定时器只是为了延迟,所以不需要返回值。
  3. Timer::operator co_await(): 这个重载的运算符使得 Timer 对象可以用在 co_await 表达式中。它创建并返回一个 Awaiter 对象,该对象负责挂起和恢复协程。
  4. Task 结构体: 这是一个简单的协程类型,用于 my_coroutine 函数。promise_type 提供了协程 promise 的必要方法,如 get_return_object, initial_suspend, final_suspend, return_void, 和 unhandled_exception
  5. my_coroutine() 函数: 这是一个协程函数,使用 co_await Timer 来挂起自身 1 秒钟。当 Timer 到期后,协程会恢复执行,并打印第二条消息。
  6. main() 函数: 调用 my_coroutine() 启动协程。为了确保协程有足够的时间运行完成,主线程休眠 1.5 秒。因为协程在新线程中恢复,如果主线程过早退出,协程可能无法完成。

总结

co_await 关键字是 C++ 协程的核心。理解它的工作机制对于掌握协程至关重要。通过自定义 awaitable 对象,我们可以更好地理解 co_await 的原理,并将其应用于各种异步编程场景。掌握了 co_await,你就打开了 C++ 协程的大门,可以编写出更高效、更易于维护的异步代码。

深入理解 Awaiter

Awaiter 类/结构体是 co_await 机制中的关键组件。它负责管理协程的挂起和恢复。让我们更详细地分析 Awaiter 的作用和实现。

await_ready() 的作用

await_ready() 方法用于确定 awaitable 对象是否已经准备好产生结果。如果 await_ready() 返回 true,则协程不会挂起,直接跳到 await_resume() 方法。这可以避免不必要的挂起和恢复操作,提高性能。

例如,考虑一个从缓存中读取数据的场景。如果数据已经在缓存中,则 await_ready() 方法可以返回 true,直接从缓存中返回数据,而无需挂起协程等待 I/O 操作。

await_suspend() 的作用

await_suspend() 方法用于挂起协程,并将控制权返回给调用者。该方法接受一个 std::coroutine_handle<> 类型的参数,该参数表示当前协程的句柄。await_suspend() 方法可以将该句柄保存起来,并在 awaitable 对象完成时使用该句柄恢复协程的执行。

await_suspend() 方法的返回值可以是 voidboolstd::coroutine_handle<>

  • 如果返回 void,则协程将在 awaitable 对象完成时立即恢复执行。
  • 如果返回 bool,则返回 false 表示协程将在 awaitable 对象完成时立即恢复执行,返回 true 表示由 await_suspend 负责恢复协程,通常用于更复杂的调度场景。
  • 如果返回 std::coroutine_handle<>,则返回的句柄将用于恢复协程的执行。这允许 await_suspend() 方法将协程的恢复操作委托给另一个协程。

await_resume() 的作用

await_resume() 方法用于返回 awaitable 对象的结果。该方法的返回值将作为 co_await 表达式的值。

await_resume() 方法可以抛出异常。如果 await_resume() 方法抛出异常,则该异常将被传递给协程的调用者。

异常处理

在协程中,异常处理是一个重要的考虑因素。如果 awaitable 对象在等待过程中发生异常,则该异常应该被传递给协程的调用者。这可以通过在 await_resume() 方法中抛出异常来实现。

例如:

struct MyAwaitable {
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) noexcept {
// ...
}
int await_resume() {
if (/* error condition */) {
throw std::runtime_error("Something went wrong");
}
return 42;
}
};
Task my_coroutine() {
try {
int result = co_await MyAwaitable{};
std::cout << "Result: " << result << std::endl;
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
}

在这个示例中,如果 MyAwaitable::await_resume() 方法检测到错误条件,它将抛出一个 std::runtime_error 异常。该异常将被 my_coroutine() 函数中的 try...catch 块捕获,并打印错误信息。

C++ 协程的优势

使用 C++ 协程进行异步编程具有以下优势:

  • 代码可读性:协程允许你以同步的方式编写异步代码,从而提高代码的可读性和可维护性。
  • 性能:协程避免了线程切换的开销,从而提高性能。
  • 灵活性:协程可以用于各种异步编程场景,例如 I/O 操作、定时器、并发任务等。

总结

co_await 是 C++ 协程的核心,理解它的工作机制对于掌握协程至关重要。通过自定义 awaitable 对象,我们可以更好地理解 co_await 的原理,并将其应用于各种异步编程场景。C++ 协程为异步编程带来了新的范式,可以编写出更高效、更易于维护的异步代码。希望本文能够帮助你更好地理解和运用 C++ 协程。

CoroutineMaster C++协程co_await

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9314