1. 协程基础与离线任务场景
1.1 协程核心概念
协程(Coroutine)是一种在单线程内实现协作式多任务的控制结构。与线程不同,协程通过显式的挂起(suspend)和恢复(resume)点来切换执行流,不依赖操作系统内核调度。在C++20标准中,协程以三个核心关键字实现:co_await、co_yield、co_return,配合编译器自动生成的框架状态机,将函数拆解为可暂停的执行步骤。
协程与线程的核心差异
- 调度方式:协程由开发者控制挂起点,用户态切换(约 10~100 ns);线程由内核抢占式调度,上下文切换开销大(约 1~10 μs)。
- 内存占用:协程栈通常分配在堆上,启动时仅分配少量固定大小(如 4 KB);线程默认栈 1~8 MB。
- 并发模型:协程适合 I/O 密集型、任务流分阶段执行的场景;线程适合 CPU 密集、需要利用多核的场景。
1.2 离线任务的特点与挑战
离线任务指不需要用户实时等待结果、可在后台异步完成的批处理或重计算过程,例如:
- 数据 ETL(提取、转换、加载)
- 日志压缩与归档
- 模型训练数据预处理
- 视频转码与缩略图生成
- 定时报表生成
这类任务通常具有以下特点:
| 特点 | 说明 |
|---|---|
| 长耗时 | 单任务可运行数分钟至数小时 |
| 阶段性强 | 可拆分为读输入、计算、写输出等独立阶段 |
| 资源敏感 | 对内存、磁盘 I/O、网络带宽有需求 |
| 失败可重试 | 需要断点续传或幂等重试 |
| 依赖关系复杂 | 任务之间存在 DAG(有向无环图)依赖 |
常见陷阱:使用线程池管理离线任务时,若任务阻塞在 I/O 或等待子任务完成,线程资源会长时间被占用,导致吞吐量下降。协程的“可挂起”特性允许在等待期间主动让出执行权,避免资源浪费。
1.3 协程管理离线任务的核心优势
将协程应用于离线任务流,可以带来以下关键收益:
- 极低上下文切换开销:协程切换只在用户态执行几条指令,无系统调用,适合大量并发任务(万级别)。
- 自然表达异步流水线:通过
co_await直接等待 I/O 完成,代码保持线性风格,避免回调地狱。 - 简化任务依赖管理:可以用协程链式调用,或通过
when_all/when_any组合原语实现 DAG 调度。 - 资源按需分配:协程栈可动态增长,仅在需要时分配,对内存敏感型任务友好。
- 统一取消与超时机制:协程支持优雅取消(通过
std::stop_或自定义 promise),方便管理超时任务。
实践经验:在 C++20 中,实现协程管理离线任务流时,建议搭配 std::latch 或自定义事件循环,避免协程长期占用线程导致其他任务饥饿。典型架构为“N 个工作线程 + M 协程(M >> N)”。
1.4 协程与离线任务流关系图
1.5 典型代码结构示意
下面展示一个简化版协程离线任务生成器(基于 C++20 协程标准库)。实际项目中应将此结构嵌入事件循环:
#include <coroutine>
#include <cstdint>
#include <vector>
struct Task {
struct promise_type {
auto get_return_object() { return Task{*this}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() noexcept {}
void unhandled_exception() { std::terminate(); }
};
using handle_t = std::coroutine_handle<promise_type>;
Task(promise_type& p) : handle(handle_t::from_promise(p)) {}
~Task() { if (handle) handle.destroy(); }
handle_t handle;
};
Task process_file(const char* path) {
// 模拟读取阶段
// co_await async_read(path);
// 模拟计算阶段
// co_await cpu_intensive();
// 模拟写入阶段
// co_await async_write(path + ".out");
co_return;
}
关键点:initial_suspend 返回 std::suspend_never 表示协程创建后立即开始执行;若需要延迟执行,可改为 std::suspend_always,由调度器显式恢复。离线任务流通常使用后者,以便在任务依赖就绪后再启动。