WEBKT

C++20 协程“完全”使用指南:从原理到 Ranges 实战

124 0 1 0

1. 协程的基本概念

1.1 什么是协程?

1.2 协程的组成部分

1.3 co_return, co_yield, co_await

2. 协程的创建和启动

2.1 定义 Promise Object

2.2 创建协程函数

2.3 启动协程

3. 协程的挂起和恢复

3.1 可等待对象(Awaitable Object)

3.2 创建可等待对象

3.3 使用 co_await 挂起和恢复协程

4. 协程与 Ranges 的结合使用

4.1 使用 co_yield 创建生成器

4.2 使用 Ranges 处理异步数据流

4.3 实际应用场景

5. 协程的错误处理

5.1 使用 try-catch 块捕获异常

5.2 在 Promise Object 中处理异常

5.3 使用 std::exception_ptr 传递异常

6. 协程的取消

6.1 使用 std::atomic 标记取消状态

6.2 使用 std::shared_ptr 管理协程的生命周期

7. 总结

C++20 引入的协程(Coroutines)无疑是近年来 C++ 语言最重要的特性之一。它提供了一种高效、简洁的方式来编写异步和并发代码,极大地提升了 C++ 在高并发场景下的竞争力。 但是,C++ 协程的学习曲线相对陡峭,涉及的概念和机制较为复杂。许多开发者在初次接触时常常感到困惑,不知道如何正确地使用协程来解决实际问题。

本文将深入探讨 C++20 协程的各个方面,从最基本的概念入手,逐步讲解协程的创建、挂起、恢复,以及协程与 Ranges 的结合使用。 通过大量的示例代码和实际应用场景,帮助你彻底掌握 C++ 协程,并在你的项目中发挥其强大的威力。

1. 协程的基本概念

在深入了解 C++ 协程的具体用法之前,我们需要先搞清楚几个关键概念。

1.1 什么是协程?

简单来说,协程是一种用户态的轻量级线程。与操作系统内核管理的线程不同,协程的调度完全由用户程序控制。 这使得协程的切换开销非常小,可以轻松创建成千上万个协程,而不会给系统带来过重的负担。

更重要的是,协程允许函数在执行过程中挂起(suspend)并在稍后恢复(resume)。 这种挂起和恢复的能力,使得我们可以用同步的方式编写异步代码,极大地简化了异步编程的复杂度。

1.2 协程的组成部分

一个 C++ 协程主要由以下几个部分组成:

  • Coroutine State(协程状态): 存储协程的局部变量、参数和恢复点等信息。协程状态通常在堆上分配,以便在协程挂起后仍然可以访问。
  • Promise Object(承诺对象): 协程的入口点,负责创建协程状态、处理协程的返回值和异常,以及控制协程的生命周期。
  • Coroutine Handle(协程句柄): 一个指向协程状态的指针,用于恢复协程的执行。
  • Awaitable Object(可等待对象): 用于控制协程的挂起和恢复。当协程遇到一个可等待对象时,它可以选择挂起自己,并在稍后由其他协程或线程恢复执行。

1.3 co_return, co_yield, co_await

C++ 协程引入了三个新的关键字,用于控制协程的行为:

  • co_return: 用于从协程返回值。与普通的 return 语句不同,co_return 会调用 Promise Object 的 return_valuereturn_void 方法,以便在协程结束时执行一些清理工作。
  • co_yield: 用于生成一个序列的值。带有 co_yield 的协程通常用于实现生成器(Generator),可以按需生成数据,而无需一次性将所有数据加载到内存中。
  • co_await: 用于挂起协程,等待一个可等待对象完成。当协程遇到 co_await 表达式时,它会检查可等待对象是否已经准备好。如果准备好了,协程会继续执行;否则,协程会挂起自己,并在可等待对象完成时恢复执行。

2. 协程的创建和启动

要创建一个协程,首先需要定义一个 Promise Object。Promise Object 负责创建协程状态、处理协程的返回值和异常,以及控制协程的生命周期。

2.1 定义 Promise Object

一个典型的 Promise Object 应该包含以下几个成员函数:

  • initial_suspend(): 协程启动时立即调用的函数,用于决定协程是否应该立即挂起。通常返回 std::suspend_alwaysstd::suspend_never
  • final_suspend(): 协程结束时调用的函数,用于决定协程是否应该保持挂起状态。通常返回 std::suspend_always,以便在协程结束后可以安全地销毁协程状态。
  • get_return_object(): 用于创建协程的返回值。通常返回一个 Coroutine Handle,以便可以从协程外部控制协程的执行。
  • unhandled_exception(): 当协程抛出未处理的异常时调用的函数。可以在这里记录异常信息或执行其他清理工作。
  • return_void()return_value(value): 当协程执行 co_return 语句时调用的函数。用于处理协程的返回值。
  • await_transform(awaitable): 用于转换 co_await 表达式中的可等待对象。这是一个可选的函数,可以用于自定义 co_await 的行为。

下面是一个简单的 Promise Object 的示例:

#include <coroutine>
#include <iostream>
struct MyCoroutine {
struct promise_type {
int value;
MyCoroutine get_return_object() {
return MyCoroutine{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
std::suspend_always yield_value(int value) {
this->value = value;
return {};
}
};
std::coroutine_handle<promise_type> handle;
};

2.2 创建协程函数

定义好 Promise Object 之后,就可以创建一个协程函数了。协程函数与普通函数的区别在于,它必须包含 co_returnco_yieldco_await 语句。

下面是一个简单的协程函数的示例:

MyCoroutine my_coroutine() {
std::cout << "Coroutine started" << std::endl;
co_yield 1;
std::cout << "Coroutine resumed" << std::endl;
co_yield 2;
std::cout << "Coroutine finished" << std::endl;
}

2.3 启动协程

要启动一个协程,只需要像调用普通函数一样调用协程函数即可。协程函数会返回一个 Coroutine Handle,可以使用该句柄来控制协程的执行。

int main() {
MyCoroutine coroutine = my_coroutine();
auto handle = coroutine.handle;
if (handle) {
handle.resume(); // Output: Coroutine started
std::cout << "Value: " << handle.promise().value << std::endl; // Output: Value: 1
handle.resume(); // Output: Coroutine resumed
std::cout << "Value: " << handle.promise().value << std::endl; // Output: Value: 2
handle.resume(); // Output: Coroutine finished
handle.destroy();
}
return 0;
}

3. 协程的挂起和恢复

协程最核心的特性就是它的挂起和恢复能力。通过 co_await 表达式,协程可以在执行过程中挂起自己,并在稍后由其他协程或线程恢复执行。 这使得我们可以用同步的方式编写异步代码,极大地简化了异步编程的复杂度。

3.1 可等待对象(Awaitable Object)

co_await 表达式的操作数必须是一个可等待对象。可等待对象是一个实现了 await_readyawait_suspendawait_resume 三个成员函数的对象。

  • await_ready(): 用于检查可等待对象是否已经准备好。如果准备好了,返回 true;否则,返回 false
  • await_suspend(coroutine_handle): 用于挂起协程。该函数接受一个 Coroutine Handle 作为参数,可以使用该句柄来恢复协程的执行。await_suspend 可以返回 voidboolcoroutine_handle
  • await_resume(): 用于恢复协程的执行。该函数返回 co_await 表达式的结果。

3.2 创建可等待对象

下面是一个简单的可等待对象的示例:

#include <future>
struct MyAwaitable {
std::future<int> future;
bool await_ready() {
return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
}
void await_suspend(std::coroutine_handle<> handle) {
std::thread([handle, this]() {
future.get(); // 等待 future 完成
handle.resume(); // 恢复协程
}).detach();
}
int await_resume() {
return future.get();
}
};

3.3 使用 co_await 挂起和恢复协程

有了可等待对象,就可以使用 co_await 表达式来挂起和恢复协程了。

#include <iostream>
MyCoroutine my_coroutine() {
std::promise<int> promise;
MyAwaitable awaitable{promise.get_future()};
std::cout << "Coroutine started" << std::endl;
int result = co_await awaitable;
std::cout << "Coroutine resumed with result: " << result << std::endl;
co_return;
}
int main() {
MyCoroutine coroutine = my_coroutine();
auto handle = coroutine.handle;
if (handle) {
handle.resume(); // Output: Coroutine started
std::cout << "Main thread: setting promise value" << std::endl;
promise.set_value(42);
handle.destroy();
}
return 0;
}

4. 协程与 Ranges 的结合使用

C++20 的 Ranges 库提供了一种简洁、高效的方式来处理序列数据。 协程与 Ranges 的结合使用,可以让我们以一种声明式的方式编写复杂的异步数据处理流程。

4.1 使用 co_yield 创建生成器

带有 co_yield 语句的协程可以用于创建生成器。生成器可以按需生成数据,而无需一次性将所有数据加载到内存中。 这对于处理大型数据集或需要实时生成数据的场景非常有用。

#include <iostream>
#include <vector>
#include <ranges>
struct Generator {
struct promise_type {
int value;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
void return_void() {}
void unhandled_exception() {}
std::suspend_always yield_value(int value) {
this->value = value;
return {};
}
};
std::coroutine_handle<promise_type> handle;
class iterator {
public:
using iterator_category = std::input_iterator_tag;
using value_type = int;
using difference_type = std::ptrdiff_t;
using pointer = const int*;
using reference = const int&;
iterator(std::coroutine_handle<promise_type> h) : handle(h) {}
iterator& operator++() {
handle.resume();
return *this;
}
int operator*() const { return handle.promise().value; }
bool operator==(const iterator& other) const {
return handle == nullptr || handle.done();
}
bool operator!=(const iterator& other) const { return !(*this == other); }
private:
std::coroutine_handle<promise_type> handle;
};
iterator begin() { return iterator(handle); }
iterator end() { return iterator(nullptr); }
};
Generator generate_numbers(int start, int end) {
for (int i = start; i <= end; ++i) {
co_yield i;
}
}
int main() {
auto numbers = generate_numbers(1, 5);
for (int number : numbers) {
std::cout << number << " "; // Output: 1 2 3 4 5
}
std::cout << std::endl;
return 0;
}

4.2 使用 Ranges 处理异步数据流

可以将生成器与 Ranges 库结合使用,以一种声明式的方式处理异步数据流。例如,可以使用 std::views::filter 来过滤生成器生成的数据,使用 std::views::transform 来转换生成器生成的数据。

#include <iostream>
#include <vector>
#include <ranges>
int main() {
auto numbers = generate_numbers(1, 10);
auto even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; });
auto squared_even_numbers = even_numbers | std::views::transform([](int n) { return n * n; });
for (int number : squared_even_numbers) {
std::cout << number << " "; // Output: 4 16 36 64 100
}
std::cout << std::endl;
return 0;
}

4.3 实际应用场景

协程与 Ranges 的结合使用在许多实际应用场景中都非常有用。例如,可以使用协程和 Ranges 来实现一个异步的 HTTP 服务器,可以同时处理多个客户端的请求,而不会阻塞主线程。 还可以使用协程和 Ranges 来实现一个流式数据处理管道,可以实时处理来自传感器或其他数据源的数据。

5. 协程的错误处理

在使用协程时,错误处理是一个非常重要的问题。由于协程的执行流程比较复杂,错误可能会在不同的地方发生。 因此,需要采取一些特殊的措施来确保协程的错误能够被正确地处理。

5.1 使用 try-catch 块捕获异常

最基本的错误处理方式是使用 try-catch 块来捕获异常。可以在协程函数中使用 try-catch 块来捕获可能抛出的异常。

MyCoroutine my_coroutine() {
try {
std::cout << "Coroutine started" << std::endl;
co_await MyAwaitable{};
std::cout << "Coroutine resumed" << std::endl;
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
co_return;
}

5.2 在 Promise Object 中处理异常

当协程抛出未处理的异常时,Promise Object 的 unhandled_exception 方法会被调用。可以在该方法中记录异常信息或执行其他清理工作。

struct MyCoroutine {
struct promise_type {
MyCoroutine get_return_object() {
return MyCoroutine{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {
try {
throw;
} catch (const std::exception& e) {
std::cerr << "Exception caught in promise: " << e.what() << std::endl;
}
}
};
std::coroutine_handle<promise_type> handle;
};

5.3 使用 std::exception_ptr 传递异常

可以使用 std::exception_ptr 来在协程之间传递异常。例如,可以在可等待对象的 await_suspend 方法中捕获异常,并将异常存储到 std::exception_ptr 中。 然后,可以在 await_resume 方法中重新抛出该异常。

6. 协程的取消

在某些情况下,可能需要取消一个正在执行的协程。例如,当用户关闭一个连接时,可能需要取消正在处理该连接的协程。 C++ 协程本身并没有提供取消机制,但可以通过一些技巧来实现协程的取消。

6.1 使用 std::atomic<bool> 标记取消状态

可以使用一个 std::atomic<bool> 变量来标记协程的取消状态。在协程中,定期检查该变量的值,如果该变量的值为 true,则立即退出协程。

#include <atomic>
std::atomic<bool> cancelled{false};
MyCoroutine my_coroutine() {
std::cout << "Coroutine started" << std::endl;
while (!cancelled) {
std::cout << "Coroutine running" << std::endl;
co_await std::suspend_never{};
}
std::cout << "Coroutine cancelled" << std::endl;
co_return;
}
int main() {
MyCoroutine coroutine = my_coroutine();
auto handle = coroutine.handle;
if (handle) {
handle.resume();
std::this_thread::sleep_for(std::chrono::seconds(1));
cancelled = true;
handle.resume();
handle.destroy();
}
return 0;
}

6.2 使用 std::shared_ptr 管理协程的生命周期

可以使用 std::shared_ptr 来管理协程的生命周期。当需要取消协程时,只需要将 std::shared_ptr 置空即可。 这会导致协程状态被销毁,从而取消协程的执行。

7. 总结

C++20 协程是一个强大的工具,可以用于编写高效、简洁的异步和并发代码。通过深入了解协程的各个方面,并结合实际应用场景进行练习,你可以彻底掌握 C++ 协程,并在你的项目中发挥其强大的威力。 掌握 C++ 协程无疑会提升你在现代 C++ 开发领域的竞争力。

希望本文能够帮助你更好地理解和使用 C++20 协程。 祝你在 C++ 协程的学习和实践中取得成功!

Coroutine大师 C++20协程Ranges

评论点评

打赏赞助
sponsor

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

分享

QRcode

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