C++20 中协程如何替代传统线程?

协程(Coroutines)是 C++20 标准引入的一种轻量级协作式多任务机制。它与传统的线程相比,具有更低的创建销毁开销、更高的执行效率以及更易于书写异步代码的优势。下面我们从概念、实现细节以及实际使用场景三方面深入探讨,帮助你了解如何使用协程来替代传统线程。

1. 协程概念回顾

  • 协程与线程的区别
    • 线程:系统级调度,具备独立的栈空间;线程切换涉及上下文保存、调度器调度,开销大。
    • 协程:用户级调度,基于单线程的执行上下文切换;切换只需保存寄存器和局部变量,开销极低。
  • 协程的核心
    1. 悬挂点(co_await:协程在此点暂停,等待某个异步事件完成。
    2. 恢复点(co_return/co_yield:协程恢复执行,返回结果或生成值。
    3. 协程句柄(std::coroutine_handle:用于手动控制协程的生命周期和恢复。

2. 基础语法与示例

下面给出一个最小的协程示例,展示协程如何暂停与恢复:

#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>

// 1. 协程的 promise type
struct simple_promise {
    int value = 0;
    simple_promise() {}
    ~simple_promise() {}

    // 协程入口
    auto get_return_object() { return std::coroutine_handle <simple_promise>::from_promise(*this); }
    // 进入协程时调用
    std::suspend_never initial_suspend() { return {}; }
    // 协程退出时调用
    std::suspend_always final_suspend() noexcept { return {}; }
    // 返回值
    void return_value(int v) { value = v; }
    // 异常处理
    void unhandled_exception() { std::terminate(); }
};

using simple_coroutine = std::coroutine_handle <simple_promise>;

int main() {
    // 2. 协程体
    auto coro = []() -> simple_coroutine {
        std::cout << "协程开始\n";
        co_await std::suspend_always();  // 暂停
        std::cout << "协程恢复\n";
        co_return 42;
    }();

    // 3. 主线程控制
    std::cout << "主线程等待\n";
    coro.resume();  // 恢复协程
    std::cout << "主线程继续\n";
    coro.destroy(); // 释放资源
    return 0;
}

输出

主线程等待
协程开始
协程恢复
主线程继续

关键点说明

  • co_await std::suspend_always() 使协程暂停,控制权返回给调用者。
  • coro.resume() 恢复协程执行,直到下一个暂停点。
  • coro.destroy() 释放协程占用的资源。

3. 协程替代线程的典型场景

场景 传统实现 协程实现
IO 密集型 多线程 + 阻塞 IO 单线程 + 非阻塞 IO + co_await
任务调度 std::async / std::thread std::async + co_await / 自定义调度器
状态机 复杂 if/else + 回调 co_yield 生成状态流
并发队列 生产者消费者 协程生成/消费,使用 co_yield 共享

例子:协程版异步文件读取

#include <iostream>
#include <coroutine>
#include <fstream>
#include <string>

struct async_line {
    struct promise_type {
        std::string line;
        std::string current;
        std::ifstream file;
        async_line get_return_object() { return async_line{std::coroutine_handle <promise_type>::from_promise(*this)}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle <promise_type> coro;
    bool next() {
        if (!coro.done()) coro.resume();
        return !coro.done();
    }
    std::string value() { return coro.promise().current; }
};

async_line read_lines(const std::string& filename) {
    std::ifstream file(filename);
    std::string line;
    while (std::getline(file, line)) {
        co_yield line;  // 暂停并返回一行
    }
}

使用方式:

int main() {
    for (auto line : read_lines("sample.txt")) {
        std::cout << line << '\n';
    }
}

与传统线程对比

  • 创建销毁成本:协程的创建几乎没有额外栈空间,销毁是 coro.destroy(),比 std::thread 低得多。
  • 上下文切换:协程切换仅保存寄存器,耗时微秒;线程切换需内核态切换,耗时毫秒。
  • 调度器灵活:你可以自定义事件循环,按需执行协程;线程受限于 OS 的调度策略。

4. 协程的常见陷阱

错误 说明 解决方案
忘记 resume() 协程会一直挂起 确认每个 co_await 之后都有 resume()
使用悬挂对象的生命周期 co_await 期间对象已析构 确保协程句柄存活,或使用 std::shared_ptr
异常未处理 协程内部异常会导致程序崩溃 promise_type::unhandled_exception() 处理或 try/catch
无事件循环 协程挂起但没有恢复点 需要事件循环或手动调用 resume()

5. 未来展望

  • 协程与多核并行:C++23 继续完善协程与 std::thread 的结合,提供更高层的并行 API。
  • 异步 I/O 集成:与 std::experimental::net 或第三方库(如 Boost.Asio)无缝配合,实现真正的“异步 I/O”。
  • 协程调试工具:IDE 生态逐步加入协程调试器,帮助定位 co_await 的执行路径。

6. 小结

  • 协程是 C++20 的核心异步编程技术,能够在单线程或轻量级多线程环境下实现高并发。
  • 它通过 co_await/co_yield 提供天然的挂起与恢复机制,显著降低上下文切换成本。
  • 在 IO 密集型、状态机、任务调度等场景中,协程往往能替代传统线程,代码更简洁、性能更优。

实践建议:在已有多线程项目中,先从单线程协程试点开始(如网络请求、文件 IO),逐步将关键路径迁移到协程,验证性能提升后再扩展到多线程协程混合模式。祝你在 C++20 协程世界里玩得开心!

发表评论