在C++20引入的协程框架中,std::coroutine_handle扮演着底层控制枢纽的角色。这个看似简单的类模板实际上封装了协程状态机的控制权,允许开发者直接操纵协程生命周期。与高级接口如generator或task不同,协程句柄提供的是最接近机器层面的操作能力。
协程句柄的核心能力体现在三个方面:第一,它保存了协程帧的地址,这是协程所有局部状态和挂起点的存储位置;第二,它提供了resume()和destroy()这两个关键方法,分别用于恢复执行和销毁资源;第三,通过静态成员from_promise()和promise()方法,实现了与promise对象的双向交互。这种设计使得开发者能够在必要时绕过编译器生成的包装代码,直接进行精细控制。
标准库提供了两种主要的协程句柄特化版本:
cpp复制template<typename Promise = void>
class coroutine_handle;
template<>
class coroutine_handle<void>;
非特化版本(void类型)是最基础的句柄形式,仅支持resume和destroy操作。而带Promise类型的特化版本增加了promise访问能力,这是实现协程-调用方双向通信的关键。例如,当我们需要从协程内部访问其关联的promise对象时:
cpp复制auto my_coroutine() -> Generator<int> {
co_await std::suspend_always{};
// 获取promise对象
auto& p = std::coroutine_handle<Generator<int>::promise_type>
::from_address(h.address()).promise();
}
类型系统的这种设计既保证了基础功能的通用性,又通过模板特化实现了类型安全的promise访问。值得注意的是,不同Promise类型的句柄之间不能隐式转换,这避免了潜在的类型混淆问题。
最直接的恢复方式是调用resume()成员函数:
cpp复制std::coroutine_handle<> h = create_coroutine();
h.resume(); // 首次恢复
h.resume(); // 再次恢复
这种模式适用于简单的控制场景,但缺乏错误处理和状态检查。更健壮的实现应该包含状态验证:
cpp复制if (!h.done()) {
try {
h.resume();
} catch (...) {
h.destroy();
throw;
}
}
协程句柄的普通指针特性(通过address()/from_address()转换)使其可以安全地跨越函数边界传递:
cpp复制void resume_in_different_thread(std::coroutine_handle<>* h) {
std::thread t([h]{
if (!h->done()) h->resume();
});
t.detach();
}
这种能力在实现异步调度器时尤为重要,但需要注意线程安全问题。标准不保证协程帧的线程安全性,必要时应添加同步机制。
通过promise()方法可以直接操作关联的promise对象,这为自定义协程行为提供了可能:
cpp复制template<typename T>
class Task {
public:
bool is_ready() const {
return handle_.promise().is_ready_;
}
void set_ready(bool ready) {
handle_.promise().is_ready_ = ready;
}
private:
std::coroutine_handle<promise_type> handle_;
};
这种交互模式常用于实现条件恢复——只有当特定条件满足时才实际恢复协程执行。
与RAII包装器不同,原始协程句柄不自动管理生命周期。典型的手动销毁模式包括:
cpp复制void process_coroutine() {
auto h = create_coroutine();
try {
while (!h.done()) {
h.resume();
}
} catch (...) {
h.destroy();
throw;
}
h.destroy();
}
特别需要注意的是,即使协程已经自然结束(done()返回true),也必须显式调用destroy()来释放资源。
由于协程句柄本质上是裸指针的包装,可能产生悬挂引用问题。防御性编程建议:
cpp复制class CoroutineWrapper {
public:
~CoroutineWrapper() {
if (handle_ && !handle_.done()) {
handle_.destroy();
}
}
// ...
private:
std::coroutine_handle<> handle_;
};
通过协程句柄可以实现手动的协程组合:
cpp复制auto nested_coroutine() -> Generator<int> {
auto inner = create_inner_coroutine();
while (!inner.done()) {
inner.resume();
if (inner.promise().has_value()) {
co_yield inner.promise().value();
}
}
}
这种模式比co_await更底层,但也更灵活,可以实现自定义的协程调度策略。
标准协程中异常会自动传播到恢复点,但通过句柄可以拦截异常:
cpp复制void safe_resume(std::coroutine_handle<> h) {
try {
h.resume();
} catch (const std::exception& e) {
h.promise().last_error = e.what();
h.destroy();
}
}
resume()操作的成本主要来自:
在性能敏感场景,可以通过以下方式优化:
通过重载operator new可以实现自定义内存分配:
cpp复制struct PromiseType {
static void* operator new(size_t size) {
return my_pool.allocate(size);
}
static void operator delete(void* ptr) {
my_pool.deallocate(ptr);
}
};
这种技术可以显著降低高频协程创建/销毁的开销。
构建简单的轮询调度器:
cpp复制class IOScheduler {
public:
void register_coroutine(std::coroutine_handle<> h) {
pending_.push_back(h);
}
void run() {
while (!pending_.empty()) {
auto h = pending_.front();
pending_.pop_front();
if (!h.done()) h.resume();
else h.destroy();
}
}
private:
std::deque<std::coroutine_handle<>> pending_;
};
实现类Unix的管道模式:
cpp复制template<typename T>
Generator<T> pipe(Generator<T> source, Generator<T> filter) {
while (!source.done()) {
auto val = source.next();
filter.send(val);
co_yield filter.next();
}
}
cpp复制h.destroy();
h.destroy(); // UB
cpp复制{
auto h = create_coroutine();
}
h.resume(); // 危险!
cpp复制std::coroutine_handle<A> h1 = ...;
std::coroutine_handle<B> h2 = h1; // 编译错误
cpp复制std::cout << "Coroutine frame at: " << h.address() << "\n";
cpp复制struct PromiseType {
std::string current_state = "init";
// ...
};