从promise_type::final_suspend返回std::suspend_always导致内存泄漏
C++协程内存泄漏问题分析
问题背景
我编写了以下简单的C++协程示例:
#include <cassert> // std::assert #include <coroutine> // std::suspend_never, suspend_always #include <cstdio> // printf class test { public: class promise_type { public: explicit promise_type() {} void get_return_object() const {} std::suspend_never initial_suspend() const { return {}; } void unhandled_exception() const { assert(false); } void return_void() const {} std::suspend_always final_suspend() const noexcept { return {}; } }; }; test process() { co_return; } int main() { process(); }
使用GCC(带-fcoroutines参数)或Clang编译后,用Valgrind运行可执行文件,得到如下内存泄漏报告:
==135311== Memcheck, a memory error detector ==135311== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. ==135311== Using Valgrind-3.20.0 and LibVEX; rerun with -h for copyright info ==135311== Command: test ==135311== ==135311== ==135311== HEAP SUMMARY: ==135311== in use at exit: 40 bytes in 1 blocks ==135311== total heap usage: 2 allocs, 1 frees, 72,744 bytes allocated ==135311== ==135311== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==135311== at 0x4842003: operator new(unsigned long) (vg_replace_malloc.c:434) ==135311== by 0x1091BE: process() (test.cpp:25) ==135311== by 0x109466: main (test.cpp:28) ==135311== ==135311== LEAK SUMMARY: ==135311== definitely lost: 40 bytes in 1 blocks ==135311== indirectly lost: 0 bytes in 0 blocks ==135311== possibly lost: 0 bytes in 0 blocks ==135311== still reachable: 0 bytes in 0 blocks ==135311== suppressed: 0 bytes in 0 blocks ==135311== ==135311== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
当promise_type::final_suspend返回std::suspend_never时,不会检测到内存泄漏,但根据C++标准及相关资料,调用promise_type::final_suspend后恢复协程属于未定义行为。请问该内存泄漏是否属于预期情况?有什么方法可以避免它?
问题解答
1. 内存泄漏属于预期行为
当final_suspend返回suspend_always时,协程会在执行完co_return后的最终挂起点暂停。此时C++协程框架不会自动销毁协程帧(即Valgrind检测到的40字节内存块)——因为框架设计上保留了恢复协程的可能性,但标准明确规定此时恢复协程属于未定义行为,所以框架不会主动释放这块内存,导致内存泄漏。
而当final_suspend返回suspend_never时,协程会直接结束执行,框架会自动销毁协程帧并释放内存,因此不会出现泄漏。
2. 避免内存泄漏的方法
核心思路是手动管理协程帧的生命周期:让协程的返回对象持有协程句柄,在对象析构时主动销毁协程帧。具体修改如下:
#include <cassert> #include <coroutine> #include <cstdio> class test { public: class promise_type { public: explicit promise_type() {} // 修改get_return_object,返回持有协程句柄的test对象 test get_return_object() { return test{std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_never initial_suspend() const { return {}; } void unhandled_exception() const { assert(false); } void return_void() const {} std::suspend_always final_suspend() const noexcept { return {}; } }; // 持有协程句柄 std::coroutine_handle<promise_type> handle; // 构造函数:初始化句柄 explicit test(std::coroutine_handle<promise_type> h) : handle(h) {} // 禁止拷贝,允许移动语义避免重复销毁 test(const test&) = delete; test& operator=(const test&) = delete; test(test&& other) noexcept : handle(other.handle) { other.handle = nullptr; } test& operator=(test&& other) noexcept { if (this != &other) { if (handle) handle.destroy(); handle = other.handle; other.handle = nullptr; } return *this; } // 析构函数:销毁协程帧 ~test() { if (handle) { handle.destroy(); } } }; test process() { co_return; } int main() { auto coro = process(); }
3. 关键说明
- 标准允许在协程处于挂起状态时调用
coroutine_handle::destroy(),而final_suspend返回suspend_always时,协程正处于合法的挂起状态,因此调用destroy()不会触发未定义行为。 - 通过返回对象的析构函数自动销毁协程帧,既符合标准规范,又能确保内存被正确释放,Valgrind不会再检测到泄漏。
内容的提问来源于stack exchange,提问作者Felix Wunderlich
相关产品推荐
相关产品推荐

