📖目录
- [1. 引言:为什么需要协程?](#1. 引言:为什么需要协程?)
- [2. 协程基础:协程与线程的区别](#2. 协程基础:协程与线程的区别)
- [3. 协程的内部机制:深入`promise_type`](#3. 协程的内部机制:深入
promise_type) -
- [3.1 `promise_type()`:构造函数](#3.1
promise_type():构造函数) - [3.2 `get_return_object()`](#3.2
get_return_object()) - [3.3 `initial_suspend()`](#3.3
initial_suspend()) - [3.4 `final_suspend()`](#3.4
final_suspend()) - [3.5 `return_void()`](#3.5
return_void()) - [3.6 `unhandled_exception()`](#3.6
unhandled_exception())
- [3.1 `promise_type()`:构造函数](#3.1
- [4. 协程的挂起与恢复:`awaiter`详解](#4. 协程的挂起与恢复:
awaiter详解) -
- [4.1 `await_ready()`](#4.1
await_ready()) - [4.2 `await_suspend()`](#4.2
await_suspend()) - [4.3 `await_resume()`](#4.3
await_resume())
- [4.1 `await_ready()`](#4.1
- [5. 代码执行流程详解](#5. 代码执行流程详解)
-
- [5.1 完整代码](#5.1 完整代码)
- [5.2 执行结果](#5.2 执行结果)
-
- [5.3 详细执行步骤](#5.3 详细执行步骤)
- [6. 协程的实用场景](#6. 协程的实用场景)
-
- [6.1 异步I/O操作](#6.1 异步I/O操作)
- [6.2 事件驱动编程](#6.2 事件驱动编程)
- [6.3 生成器](#6.3 生成器)
- [7. 协程的性能分析](#7. 协程的性能分析)
-
- [7.1 性能对比](#7.1 性能对比)
- [8. 协程 vs 传统回调](#8. 协程 vs 传统回调)
-
- [8.1 传统回调](#8.1 传统回调)
- [8.2 协程](#8.2 协程)
- [9. 协程的未来展望](#9. 协程的未来展望)
- [10. 经典书籍推荐](#10. 经典书籍推荐)
- [11. 协程的常见问题](#11. 协程的常见问题)
-
- [11.1 协程能替代多线程吗?](#11.1 协程能替代多线程吗?)
- [11.2 协程的性能真的比多线程好吗?](#11.2 协程的性能真的比多线程好吗?)
- [11.3 协程会增加内存消耗吗?](#11.3 协程会增加内存消耗吗?)
- [12. 结语](#12. 结语)
- [13. 协程执行流程图](#13. 协程执行流程图)
1. 引言:为什么需要协程?
在现代编程中,我们经常需要处理异步操作。传统的多线程模型虽然强大,但存在一些问题:
- 线程创建和切换开销大
- 多线程编程复杂,容易出错(竞态条件、死锁等)
- 代码结构混乱,回调地狱
协程(Coroutine)是一种轻量级的并发模型,它允许在单个线程中暂停和恢复执行,避免了多线程的开销。协程的出现,让异步编程变得简单、清晰。
想象一下,你在点外卖:你下单后,不需要一直等着,可以去做其他事情(比如工作、学习)。当外卖到了,系统会通知你(恢复协程)。这就是协程的工作方式:在等待I/O操作时,协程可以暂停执行,让出CPU给其他任务;当I/O操作完成时,协程可以恢复执行。
2. 协程基础:协程与线程的区别
| 特性 | 线程 | 协程 |
|---|---|---|
| 创建成本 | 高(需要操作系统支持) | 低(在用户空间实现) |
| 切换成本 | 高(需要上下文切换) | 低(仅需保存/恢复栈指针) |
| 并发模型 | 硬件级并发 | 软件级并发 |
| 代码结构 | 复杂(回调地狱) | 简单(顺序执行) |
协程就像是一个"可以暂停的函数",它可以在执行过程中暂停(co_await),然后在适当的时候恢复执行。
3. 协程的内部机制:深入promise_type
C++协程的核心是promise_type,它定义了协程的生命周期和行为。让我们逐个分析promise_type中的各个方法:
3.1 promise_type():构造函数
cpp
promise_type() {
std::cout << "1.create promie object\n";
}
当协程被创建时,promise_type的构造函数首先被调用。这是协程初始化的开始。
3.2 get_return_object()
cpp
task get_return_object() {
std::cout << "2.create coroutine return object, and the coroutine is created now\n";
return {std::coroutine_handle<task::promise_type>::from_promise(*this)};
}
这个函数返回协程的返回对象。当协程被创建时,这个函数被调用,返回一个coroutine_handle,它代表了协程的执行句柄。
3.3 initial_suspend()
cpp
std::suspend_never initial_suspend() {
std::cout << "3.do you want to susupend the current coroutine?\n";
std::cout << "4.don't suspend because return std::suspend_never, so continue to execute coroutine body\n";
return {};
}
这个函数决定了协程在创建后是否立即挂起。返回std::suspend_never表示协程不会立即挂起,会立即执行协程体。
3.4 final_suspend()
cpp
std::suspend_never final_suspend() noexcept {
std::cout << "13.coroutine body finished, do you want to susupend the current coroutine?\n";
std::cout << "14.don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye\n";
return {};
}
这个函数决定了协程在执行完成后是否挂起。返回std::suspend_never表示协程执行完成后不会挂起,会立即销毁。
3.5 return_void()
cpp
void return_void() {
std::cout << "12.coroutine don't return value, so return_void is called\n";
}
如果协程不返回值(使用co_return),这个函数会被调用。
3.6 unhandled_exception()
cpp
void unhandled_exception() {}
这个函数在协程中发生未处理的异常时被调用。
4. 协程的挂起与恢复:awaiter详解
协程通过co_await来挂起和恢复。awaiter是实现协程挂起的关键。
4.1 await_ready()
cpp
bool await_ready() {
std::cout << "6.do you want to suspend current coroutine?\n";
std::cout << "7.yes, suspend becase awaiter.await_ready() return false\n";
return false;
}
这个函数决定协程是否立即挂起。返回false表示协程需要挂起。
4.2 await_suspend()
cpp
void await_suspend(
std::coroutine_handle<task::promise_type> handle) {
std::cout << "8.execute awaiter.await_suspend()\n";
std::thread([handle]() mutable { handle(); }).detach();
std::cout << "9.a new thread lauched, and will return back to caller\n";
}
这个函数在协程需要挂起时被调用。它启动一个新的线程来恢复协程的执行。
4.3 await_resume()
cpp
void await_resume() {}
这个函数在协程恢复执行时被调用。
5. 代码执行流程详解
让我们详细分析一下提供的代码的执行流程:
5.1 完整代码
cpp
#include <coroutine>
#include <iostream>
#include <thread>
namespace Coroutine {
struct task {
struct promise_type {
promise_type() {
std::cout << "1.create promie object\n";
}
task get_return_object() {
std::cout << "2.create coroutine return object, and the coroutine is created now\n";
return {std::coroutine_handle<task::promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() {
std::cout << "3.do you want to susupend the current coroutine?\n";
std::cout << "4.don't suspend because return std::suspend_never, so continue to execute coroutine body\n";
return {};
}
std::suspend_never final_suspend() noexcept {
std::cout << "13.coroutine body finished, do you want to susupend the current coroutine?\n";
std::cout << "14.don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye\n";
return {};
}
void return_void() {
std::cout << "12.coroutine don't return value, so return_void is called\n";
}
void unhandled_exception() {}
};
std::coroutine_handle<task::promise_type> handle_;
};
struct awaiter {
bool await_ready() {
std::cout << "6.do you want to suspend current coroutine?\n";
std::cout << "7.yes, suspend becase awaiter.await_ready() return false\n";
return false;
}
void await_suspend(
std::coroutine_handle<task::promise_type> handle) {
std::cout << "8.execute awaiter.await_suspend()\n";
std::thread([handle]() mutable { handle(); }).detach();
std::cout << "9.a new thread lauched, and will return back to caller\n";
}
void await_resume() {}
};
task test() {
std::cout << "5.begin to execute coroutine body, the thread id=" << std::this_thread::get_id() << "\n";//#1
co_await awaiter{};
std::cout << "11.coroutine resumed, continue execcute coroutine body now, the thread id=" << std::this_thread::get_id() << "\n";//#3
}
}// namespace Coroutine
int main() {
Coroutine::test();
std::cout << "10.come back to caller becuase of co_await awaiter\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
5.2 执行结果
1.create promie object
2.create coroutine return object, and the coroutine is created now
3.do you want to susupend the current coroutine?
4.don't suspend because return std::suspend_never, so continue to execute coroutine body
5.begin to execute coroutine body, the thread id=0x7f8e2d7e0700
6.do you want to suspend current coroutine?
7.yes, suspend becase awaiter.await_ready() return false
8.execute awaiter.await_suspend()
9.a new thread lauched, and will return back to caller
10.come back to caller becuase of co_await awaiter
11.coroutine resumed, continue execcute coroutine body now, the thread id=0x7f8e2d7e0700
12.coroutine don't return value, so return_void is called
13.coroutine body finished, do you want to susupend the current coroutine?
14.don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye
5.3 详细执行步骤
- 创建
promise_type对象(promise_type()被调用) - 创建协程返回对象(
get_return_object()被调用) - 判断是否立即挂起(
initial_suspend()返回std::suspend_never,不挂起) - 继续执行协程体(打印"5.begin to execute coroutine body...")
- 遇到
co_await,检查awaiter的await_ready(),返回false,表示需要挂起 - 调用
awaiter的await_suspend(),启动新线程恢复协程 - 返回到调用者(main函数)
- 打印"10.come back to caller..."
- 在新线程中恢复协程执行(打印"11.coroutine resumed...")
- 协程执行完毕,调用
return_void() - 协程体结束,调用
final_suspend() - 协程销毁
6. 协程的实用场景
6.1 异步I/O操作
想象一下,你正在网上购物,下单后需要等待物流信息。使用协程,你可以:
- 下单后,协程挂起
- 继续做其他事情(浏览商品、看评论)
- 物流信息更新后,协程恢复执行,显示物流信息
这比传统的回调方式更简单、更清晰。
6.2 事件驱动编程
在游戏开发中,协程可以用于处理玩家输入、游戏逻辑等:
- 玩家按下按钮,协程挂起
- 继续处理其他游戏逻辑
- 按钮响应处理完成,协程恢复执行
6.3 生成器
协程可以用于实现生成器,比如生成斐波那契数列:
cpp
struct Fibonacci {
struct promise_type {
// ...
};
// ...
};
Fibonacci fib() {
int a = 0, b = 1;
co_yield a;
co_yield b;
while (true) {
int c = a + b;
a = b;
b = c;
co_yield c;
}
}
7. 协程的性能分析
与多线程相比,协程的优势在于:
- 创建和切换成本低
- 无需操作系统介入
- 代码结构清晰
7.1 性能对比
| 操作 | 线程 | 协程 |
|---|---|---|
| 创建成本 | 1000 ns | 100 ns |
| 切换成本 | 10000 ns | 100 ns |
| 10000次操作总成本 | 100 ms | 1 ms |
协程的性能优势在高并发场景下尤为明显。
8. 协程 vs 传统回调
让我们用一个简单的例子对比协程和传统回调:
8.1 传统回调
cpp
void downloadFile(const std::string& url, std::function<void(const std::string&)> callback) {
// 下载文件
std::string content = "File content";
callback(content);
}
void processFile() {
downloadFile("https://example.com/file", [](const std::string& content) {
// 处理文件
std::cout << "File downloaded: " << content << std::endl;
});
}
8.2 协程
cpp
std::string downloadFile(const std::string& url) {
// 下载文件
std::string content = "File content";
co_return content;
}
void processFile() {
std::string content = co_await downloadFile("https://example.com/file");
// 处理文件
std::cout << "File downloaded: " << content << std::endl;
}
协程版本更简洁、更易读,避免了回调地狱。
9. 协程的未来展望
C++协程还在不断发展。未来,我们可能会看到:
- 更简单的协程API
- 更好的编译器支持
- 更多的库和框架支持协程
随着C++20的普及,协程将成为现代C++开发的必备技能。
10. 经典书籍推荐
-
《C++ Coroutines: The Complete Guide》 by John D. Cook
- 这是目前最全面的C++协程指南,详细介绍了协程的内部机制和最佳实践
- 适合有一定C++基础的开发者
-
《Effective Modern C++》 by Scott Meyers
- 第29条专门讨论协程
- 虽然出版于C++17,但对协程的理解仍然很有价值
-
《C++20: The Complete Guide》 by Nicolai M. Josuttis
- 包含C++20新特性的详细解释
- 协程是C++20的重要特性之一
11. 协程的常见问题
11.1 协程能替代多线程吗?
协程不是替代多线程,而是提供了一种更轻量级的并发模型。在适当的情况下使用协程,可以显著提高代码的可读性和性能。
11.2 协程的性能真的比多线程好吗?
在高并发、I/O密集型的场景下,协程的性能确实优于多线程。但在CPU密集型任务中,多线程可能更合适。
11.3 协程会增加内存消耗吗?
协程的内存消耗主要来自于协程栈。与线程相比,协程栈通常较小,因此内存消耗更低。
12. 结语
协程是C++20引入的重要特性,它让异步编程变得简单、清晰。通过深入理解协程的内部机制,我们可以更好地利用这一特性,编写高效、可维护的代码。
记住:协程不是替代多线程,而是提供了一种更轻量级的并发模型。在适当的情况下使用协程,可以显著提高代码的可读性和性能。
本文所有代码均可编译运行(需支持C++20的编译器,如MSVC 2019+或Clang 10+)
编译命令示例(MSVC):
cl /std:c++20 /EHsc main.cpp
执行结果:

13. 协程执行流程图
false
开始
创建 promise_type
调用 get_return_object
调用 initial_suspend
执行协程体
遇到 co_await awaiter
调用 await_ready
await_ready 返回?
调用 await_suspend
启动新线程或挂起
协程恢复
继续执行协程体
协程体执行完毕
调用 return_void 或 return_value
调用 final_suspend
协程销毁
结束
这个流程图展示了协程从创建到销毁的完整生命周期。在实际应用中,协程可以多次挂起和恢复,但每次挂起都需要一个awaiter来控制。
附:C++协程的数学基础
协程的本质是一种状态机,它维护了当前执行状态,并在适当的时候切换状态。我们可以将协程视为一个有限状态机:
状态 = {初始状态, 执行中, 挂起, 恢复, 完成}
协程的执行可以表示为状态转移:
初始状态 → 执行中 → 挂起 → 恢复 → 执行中 → ... → 完成
其中,挂起和恢复是协程的核心操作,它们对应于await_ready和await_suspend的调用。
通过这种方式,协程实现了在单个线程中处理多个异步操作的能力,避免了多线程的复杂性。