C++20 中的协程:从基本语法到应用实例

C++20 引入了协程(coroutines)这一强大的语言特性,它为异步编程、生成器、状态机以及其他需要暂停与恢复执行流的场景提供了统一且高效的实现方式。本文将从协程的概念入手,讲解基本语法、核心组件,并通过一个完整的“异步文件读取”示例,展示协程在实际项目中的应用与优势。

一、协程到底是什么?

协程是一种能够在执行过程中暂停(co_yieldco_return)并在需要时恢复的函数。与传统的线程相比,协程的上下文切换成本极低,且能够让代码保持同步化的写法,从而避免回调地狱或复杂的状态机。

C++20 对协程的实现分为三大核心:

  • awaitable:表示可等待的异步操作。
  • promise_type:协程的执行状态容器,存储协程的返回值、异常、以及调度逻辑。
  • await_transform:让协程能够“等待”任意类型的 awaitable。

二、协程的基本语法

#include <coroutine>
#include <iostream>
#include <string_view>

struct task {
    struct promise_type {
        std::string result_;
        task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_value(std::string r) { result_ = std::move(r); }
        void unhandled_exception() { std::terminate(); }
    };
};

task hello() {
    co_return "Hello, coroutine!";
}

关键点说明:

  • co_return 用于返回协程的最终值。
  • initial_suspendfinal_suspend 控制协程的起始与结束时是否挂起。
  • promise_type 是协程的状态机,实现了协程需要的各种生命周期钩子。

三、awaitable 的实现与使用

最常见的 awaitable 形态是 std::future 或自定义的 async_operation。下面给出一个自定义的异步计时器:

#include <chrono>
#include <thread>

struct async_timer {
    std::chrono::milliseconds duration_;
    async_timer(std::chrono::milliseconds d) : duration_(d) {}

    struct awaiter {
        std::chrono::milliseconds duration_;
        bool await_ready() const noexcept { return false; }
        void await_suspend(std::coroutine_handle<> h) {
            std::thread([h, d = duration_]() {
                std::this_thread::sleep_for(d);
                h.resume();
            }).detach();
        }
        void await_resume() const noexcept {}
    };

    awaiter operator co_await() const noexcept {
        return awaiter{duration_};
    }
};

使用示例:

task wait_and_print() {
    co_await async_timer( std::chrono::seconds{2} );
    std::cout << "Timer expired\n";
}

四、完整示例:异步文件读取

假设我们有一个大文本文件 data.txt,需要逐行异步读取并打印。传统方式需要多线程或事件循环;使用协程可以写成:

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

struct async_line_reader {
    std::ifstream file_;
    async_line_reader(const std::string& path) : file_(path) {}

    struct awaiter {
        std::ifstream* file_;
        std::string line_;
        bool await_ready() const noexcept { return false; }
        void await_suspend(std::coroutine_handle<> h) {
            std::thread([h, f = file_]() mutable {
                if (*f && std::getline(*f, line_)) {
                    h.resume();
                }
            }).detach();
        }
        std::string await_resume() { return line_; }
    };

    awaiter operator co_await() const noexcept {
        return awaiter{&file_, ""};
    }
};

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

async_reader_task read_file(const std::string& path) {
    async_line_reader reader(path);
    while (true) {
        std::string line = co_await reader;
        if (line.empty()) break; // EOF
        std::cout << line << '\n';
    }
}

运行 read_file("data.txt") 即可在主线程中得到异步流式读取的效果,而代码保持同步化写法。

五、协程的调度与性能

  • 调度器(Scheduler):C++ 标准库并未提供默认调度器,通常需要自己实现或使用第三方库(如 cppcoroBoost.Asio 的协程接口)。调度器负责决定何时恢复协程。
  • 状态保存:协程的局部变量会保存在堆上(通过 promise_type 或自定义 state),与线程栈相比内存开销更可控。
  • 异常传播:协程会将异常捕获到 promise_type::unhandled_exception(),保持异常链完整。

六、注意事项与常见坑

场景 建议 可能的错误
立即返回的协程 initial_suspendfinal_suspend 可设为 std::suspend_never 忽略了 co_yield 的暂停
共享状态 避免在协程内部使用裸指针 数据竞争
多线程与协程 调度器必须保证线程安全 死锁
大量协程 采用自定义堆分配器 内存碎片

七、结语

C++20 的协程为我们提供了一种既高效又易读的异步编程模型。通过正确理解 awaitablepromise_type 以及调度机制,开发者可以在不牺牲性能的前提下,以同步化的代码实现复杂的异步流程。未来的标准库和第三方生态将进一步丰富协程的调度与工具箱,值得每位 C++ 开发者持续关注。

发表评论