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

C++20协程:co_await后循环捕获异常访问e.what()触发段错误的解决方法

协程异常捕获后访问e.what()触发段错误问题

我在co_await一个抛出异常的协程后,尝试在catch块中访问异常详情(e.what())时遇到了段错误。异常能被成功捕获,但访问其任何方法都会导致崩溃。

重要提示:该问题仅在使用for循环时出现。如果移除循环,仅保留单个try { co_await task2; } catch (...)块,程序运行正常。

最小可复现代码示例

#include <Windows.h>
#include <coroutine>
#include <exception>
#include <iostream>
#include <stdexcept>

struct Task2 {
   struct promise_type {
      std::exception_ptr* exception_;

      auto get_return_object() {
         return Task2{std::coroutine_handle<promise_type>::from_promise(*this)};
      }
      std::suspend_never  initial_suspend() { return {}; }
      std::suspend_always   // 故意设置:尝试保持帧存活
                          final_suspend() noexcept { return {}; }
      void return_void() {}
      void unhandled_exception() { *exception_ = std::current_exception(); }
   };

   Task2(std::coroutine_handle<promise_type> h)
      : handle_(h) {
      h.promise().exception_ = &exception_;
   }

   ~Task2() {
      std::cout << "Task2 destroyed" << std::endl;
      if (handle_) {
         handle_.destroy();
      }
   }

   std::coroutine_handle<promise_type> handle_;
   std::exception_ptr                  exception_;

   bool await_ready() { return true; }
   void await_suspend(std::coroutine_handle<>) {}
   void await_resume() {
      if (exception_) {
         std::rethrow_exception(exception_);
      }
   }
};

struct Task1 {
   struct promise_type {
      auto get_return_object() {
         return Task1{std::coroutine_handle<promise_type>::from_promise(*this)};
      }
      std::suspend_never  initial_suspend() { return {}; }
      std::suspend_always   // 故意设置:尝试保持帧存活
                          final_suspend() noexcept { return {}; }
      void                return_void() {}
      void                unhandled_exception() {}
   };

   Task1(std::coroutine_handle<promise_type> h)
      : handle_(h) {}

   ~Task1() {
      std::cout << "Task1 destroyed" << std::endl;
      if (handle_) {
         handle_.destroy();
      }
   }

   std::coroutine_handle<promise_type> handle_;
};

Task2
FThrow() {
   throw std::runtime_error("TEST_EXCEPTION");
   co_return;
}

Task1
Fmain() {
   for (int i = 0; i < 10; ++i) {  // 问题仅在循环中出现
      std::cout << "Main promise iteration " << i << std::endl;
      auto task2 = FThrow();
      try {
         co_await task2;
      } catch (std::exception const& e) {
         std::cout << "TEST " << e.what() << std::endl;  // 此处触发段错误
      }
   }
   co_return;
}

int WINAPI
WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/, LPSTR /*lpCmdLine*/, int /*nCmdShow*/) {
   auto task = Fmain();
   return 0;
}

预期行为

程序应输出:

Main promise iteration 0
TEST TEST_EXCEPTION
Task2 destroyed
Main promise iteration 1
...

(Task1/Task2的析构函数应在循环/程序结束时调用)

实际行为

程序输出:

Main promise iteration 0
TEST 

随后在调用e.what()时崩溃并触发段错误。

关键问题:崩溃前析构函数未被调用——输出中既没有"Task2 destroyed"也没有"Task1 destroyed",这意味着段错误发生时Task2对象(及其存储的协程句柄)仍然存在。

启用页堆的WinDbg显示崩溃发生在0xCDCDCDCD(未初始化内存模式),表明异常对象已被释放。

调查

  1. 异常对象通过std::current_exception()捕获并存储在std::exception_ptr中
  2. 异常在await_resume()中被成功重新抛出并捕获
  3. 在MSVC中,被std::exception_ptr捕获的异常对象分配在协程帧(coroutine frame)中
  4. 我故意在final_suspend()中使用suspend_always,并在析构函数中手动调用destroy(),试图保持帧存活直到catch处理完成
  5. 析构函数在崩溃前未打印输出,说明段错误发生时Task2对象仍存在,但异常对象已被释放
  6. 该问题仅在循环中出现——单个try/catch可正常运行
  7. 调试观察:在第41行(std::rethrow_exception(exception_);)断点时,调试器中的exception_变量看起来有效,即使发生段错误后其地址在调试视图中仍显示正确,这表明std::exception_ptr本身是正常的。

假设

我怀疑FThrow()的协程帧(异常对象所在位置)在await_resume()重新抛出异常后、catch处理程序访问e.what()之前被销毁。

流程似乎是:

  1. FThrow()抛出异常→被unhandled_exception()捕获→存储为std::exception_ptr
  2. await_resume()调用std::rethrow_exception(exception_)→异常传播
  3. 协程帧在此处被销毁——即使持有句柄的Task2对象仍然存在
  4. 异常在catch块中被捕获,但实际异常对象已不存在
  5. 调用e.what()会解引用已释放的内存→段错误
  6. 之后Task2析构函数才会运行(但我们先崩溃了)

核心谜团:Task2析构函数在崩溃前未运行(无输出),说明Task2对象仍存在且应持有带有final_suspend()中suspend_always的协程句柄,但帧却已被销毁。

可能的解释:

  • await_ready()返回true意味着协程从未真正挂起,使得final_suspend()中的suspend_always失去意义
  • 帧在异常抛出后立即被销毁,无论挂起设置如何
  • std::exception_ptr实际上并未保持异常对象存活,仅指向它

环境

  • Clang-CL(带MSVC兼容层的LLVM/Clang编译器)[测试过多个版本]
  • MSVC标准库版本19.42(Visual Studio 2022)
  • C++20(/std:c++20)
  • Windows 11
  • 启用页堆的Debug构建

构建命令行

Clang-CL

编译命令:

C:\PROGRA~1\LLVM\bin\clang-cl.exe  /nologo -TP -DNOMINMAX /DWIN32 /D_WINDOWS /EHsc /Zi /Ob0 /Od /RTC1 -clang:-std=c++20 -MDd /W4 -clang:-DDEBUG -clang:-Wall -clang:-Wextra -clang:-Wpedantic -clang:-Wcast-align -clang:-Waddress-of-packed-member -clang:-Werror -clang:-ftemplate-backtrace-limit=0 -clang: -clang:-O0 -clang:-g /showIncludes /Fobuild\promise_test.cpp.obj /Fdbuild\ -c -- promise_test.cpp

链接命令:

C:\PROGRA~1\LLVM\bin\lld-link.exe /nologo build\promise_test.cpp.obj  /out:build\promise_test.exe /implib:build\promise_test.lib /pdb:build\promise_test.pdb /version:0.0 /machine:x64 /debug /INCREMENTAL /subsystem:windows kernel32.lib

MSVC

编译命令:

cl.exe /nologo /TP /DNOMINMAX /DWIN32 /D_WINDOWS /EHsc /Zi /Od /RTC1 /std:c++20 /MDd /W4 /DDEBUG /showIncludes /Fobuild\promise_test.cpp.obj /Fdbuild\ -c promise_test.cpp

链接命令:

link.exe /nologo build\promise_test.cpp.obj /out:build\promise_test.exe /implib:build\promise_test.lib /pdb:build\promise_test.pdb /version:0.0 /machine:x64 /debug /INCREMENTAL /subsystem:windows kernel32.lib

问题

如何在co_await后安全捕获并访问协程抛出的异常,避免出现野指针? 是否有办法确保协程帧(以及异常对象)在catch处理程序完成前保持有效,或者这是C++20协程的基本限制,需要采用不同的错误处理模式?

具体问题:

  • 我关于帧销毁时机的假设是否正确?
  • await_ready()返回true是否会绕过挂起机制并导致帧立即销毁?
  • 是否有正确的方法保持帧存活直到异常处理完成?
  • 这可能是编译器优化错误导致的吗?

更新

将异常按引用传入协程帧会重现相同的崩溃,因此问题不限于帧分配的异常对象。

int WINAPI
WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/, LPSTR /*lpCmdLine*/, int /*nCmdShow*/) {
   std::runtime_error e{"TEST_EXCEPTION"};
   auto task = Fmain(e);
   return 0;
}

...

Task2
FThrow(std::runtime_error const& e) {
   throw e;
   co_return;
}

内容的提问来源于stack exchange,提问作者Alex

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.11 13:35:54