You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 05:45:14