C++20协程:新手快速上手指南

C++20 通过引入协程(coroutine)提供了一种简洁高效的异步编程模型。与传统基于回调或线程池的方式相比,协程在可读性、性能和资源占用上都有显著优势。本文将从基本概念、关键字、实现原理、使用场景以及常见陷阱等方面,为你拆解协程的核心价值,并给出完整示例代码,帮助你快速上手。

1. 协程基础

协程是一种轻量级的“挂起”函数。它允许函数在执行过程中暂停(co_await/co_yield/co_return)并恢复,从而在单线程中实现非阻塞异步操作。

核心关键字:

  • co_await:等待一个异步结果,函数挂起。
  • co_yield:产生一个值,函数挂起。
  • co_return:返回协程结果,函数挂起。

C++20 标准库提供了 std::futurestd::promise 等协程相关类型,C++23 扩展了 std::generator 等。

2. 协程实现原理

协程在编译阶段被拆分为:

  1. 状态机:把函数拆成若干状态。
  2. promise_type:协程的执行上下文,负责存储协程结果、异常等。
  3. awaiter:决定何时挂起/恢复。

编译器通过 co_await 的返回值(awaitable)的 await_readyawait_suspendawait_resume 方法,控制协程生命周期。

3. 常见使用场景

场景 传统实现 协程实现 说明
网络请求 线程池 + 线程同步 co_await async_read/write 只需几行代码,避免回调地狱
并发流处理 std::async + 未来 `generator
read_lines()` 逐行读取不占用额外线程
UI 更新 信号槽 + 线程 co_await + dispatch_to_main() 直接在主线程恢复

4. 示例代码

4.1 简单异步读取文件

#include <iostream>
#include <fstream>
#include <coroutine>
#include <string>
#include <vector>
#include <thread>
#include <future>

struct FileReadAwaiter {
    std::string path;
    std::string result;

    bool await_ready() const noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) {
        std::thread([h, this]() {
            std::ifstream file(path);
            std::string line, content;
            while (std::getline(file, line)) content += line + '\n';
            result = content;
            h.resume();  // 恢复协程
        }).detach();
    }

    std::string await_resume() const noexcept { return result; }
};

FileReadAwaiter async_read_file(const std::string& path) {
    return FileReadAwaiter{path};
}

struct AsyncTask {
    struct promise_type {
        AsyncTask get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

async void read_and_print(const std::string& path) {
    std::string content = co_await async_read_file(path);
    std::cout << "File content:\n" << content;
}

int main() {
    read_and_print("test.txt");
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待异步线程完成
}

4.2 生成器:逐行读取文件

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

template<typename T>
struct generator {
    struct promise_type {
        T current_value;
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        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::terminate(); }
    };

    std::coroutine_handle <promise_type> h;
    generator(std::coroutine_handle <promise_type> h) : h(h) {}
    ~generator() { if (h) h.destroy(); }
    generator(const generator&) = delete;
    generator& operator=(const generator&) = delete;
    generator(generator&& other) noexcept : h(other.h) { other.h = nullptr; }
    generator& operator=(generator&& other) noexcept {
        if (this != &other) { if (h) h.destroy(); h = other.h; other.h = nullptr; }
        return *this;
    }

    bool next() {
        if (!h.done()) h.resume();
        return !h.done();
    }

    T value() const { return h.promise().current_value; }
};

generator<std::string> read_lines(const std::string& path) {
    std::ifstream file(path);
    std::string line;
    while (std::getline(file, line)) co_yield line;
}

int main() {
    for (auto g = read_lines("test.txt"); g.next(); ) {
        std::cout << g.value() << '\n';
    }
}

5. 性能与资源对比

指标 传统线程池 协程 备注
CPU 开销 线程切换、上下文切换 状态机恢复 协程无线程切换
内存占用 每线程 1-2 MB 协程栈默认 1 KB 协程轻量
编码复杂度 回调链/Future 链 直线代码 可读性提升

6. 常见陷阱

  1. 忘记 co_returnreturn:协程默认会在函数末尾自动返回 co_return,但如果提前 return,可能导致协程提前结束。
  2. co_await 的 awaitable 需要满足三步协议await_readyawait_suspendawait_resume
  3. 异常传播:异常会通过 promise_type::unhandled_exception 传递,需要捕获。
  4. 协程生命周期:协程句柄必须在协程结束后销毁,否则会泄漏资源。

7. 进阶阅读

  • 《C++20 协程与异步编程》
  • 《Boost.Coroutine2 与 C++20 协程》
  • 《实战 C++20 并发》

小结

协程是 C++20 为解决异步编程痛点而推出的强大工具。通过简化异步控制流、提升性能并降低资源占用,它已成为现代 C++ 开发不可或缺的手段。掌握基本语法与实现细节后,你可以在网络、文件 I/O、UI 线程等多种场景中灵活使用协程,让代码更清晰、更高效。

发表评论