【问题标题】:How to detect stack unwinding in C++20 coroutines?如何检测 C++20 协程中的堆栈展开?
【发布时间】:2021-07-24 09:10:24
【问题描述】:

C++ 中的典型建议是使用std::uncaught_exceptions() 检测析构函数中的堆栈展开,参见https://en.cppreference.com/w/cpp/error/uncaught_exception 中的示例:

struct Foo {
    int count = std::uncaught_exceptions();
    ~Foo() {
        std::cout << (count == std::uncaught_exceptions()
            ? "~Foo() called normally\n"
            : "~Foo() called during stack unwinding\n");
    }
};

但是这个建议看起来不再适用于 C++20 协程,它可以暂停和恢复,包括在堆栈展开期间。考虑以下示例:

#include <coroutine>
#include <iostream>

struct ReturnObject {
  struct promise_type {
    ReturnObject get_return_object() { return { std::coroutine_handle<promise_type>::from_promise(*this) }; }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
    void return_void() {}
  };
  std::coroutine_handle<promise_type> h_;
};

struct Foo {
    int count = std::uncaught_exceptions();
    Foo() { std::cout << "Foo()\n"; }
    ~Foo() {
        std::cout << (count == std::uncaught_exceptions()
            ? "~Foo() called normally\n"
            : "~Foo() called during stack unwinding\n");
    }
};

struct S
{
    std::coroutine_handle<ReturnObject::promise_type> h_;
    ~S() { h_(); }
};

int main()
{
  auto coroutine = []() -> ReturnObject { Foo f; co_await std::suspend_always{}; };
  auto h = coroutine().h_;

  try
  {
      S s{ .h_ = h };
      std::cout << "Exception being thrown\n";
      throw 0; // calls s.~S() during stack unwinding
  }
  catch( int ) {}
  std::cout << "Exception caught\n";

  h();
  h.destroy();
}

它在协程中使用了相同的类Foo,该类被正常销毁(不是由于异常期间堆栈展开),但仍然打印:

Exception being thrown
Foo()
Exception caught
~Foo() called during stack unwinding

演示:https://gcc.godbolt.org/z/Yx1b18zT9

如何重新设计类 Foo 以正确检测协程中的堆栈展开?

【问题讨论】:

  • 如果 coroutine_handle 从协程的机器中泄漏,这是一种代码味道。协程故意是一个函数的实现细节。协程未来应该始终成为函数协程与外界之间的中介。
  • 你说的未来是什么意思?可能是 promise_type?
  • 不,promise 是函数协程机制定义的类型。未来是用户与之交互的类型。 “promise”存储最终值,“future”向最终接收者提供对该值的访问。未来类型是函数的返回类型,协程与否。
  • 谢谢,我明白了。至于代码,其实是根据这个手册:scs.stanford.edu/~dm/blog/c++-coroutines.html我只是简化了一点,并调整以突出显示堆栈展开的这个问题。
  • 这并没有减少代码的味道。复制future类型是一件......可疑的事情,更不用说提取coroutine_handle并直接使用它。该页面就像一个巨大的不良协程实践列表。我希望人们不要再在互联网上提供糟糕的建议。

标签: c++ c++20 c++-coroutine stack-unwinding


【解决方案1】:

想知道函数是否由于堆栈展开而正在执行的典型原因是为了回滚数据库事务。所以情况看起来是这样的:

你的函数做了一些数据库工作。它创建一个由 RAII 对象管理的数据库事务。该对象位于函数的堆栈上(直接或间接作为某个其他堆栈对象的子对象)。你做一些事情,当那个 RAII 对象离开堆栈时,数据库事务应该提交或回滚,这取决于它是正常离开堆栈还是因为异常分别通过函数本身。

这一切都非常整洁。函数本身不需要明确的清理代码。

这对协程意味着什么?这变得非常复杂,因为协程可能会因为其自身执行之外的原因而被终止

对于普通函数,它要么完成,要么抛出异常。如果这样的函数失败,它会在函数内部发生。协程不是那样工作的。在挂起点之间,安排协程恢复的代码可能本身失败。

考虑异步文件加载。您将延续函数传递给文件阅读器,延续将在读取文件数据以进行处理时获得文件数据。部分通过此过程,会发生文件读取错误。但这发生在访问文件的外部代码中,而不是消耗它的延续函数中。

所以外部代码需要告诉消费函数发生了错误,它应该中止其进程。这不能通过异常发生(至少默认情况下不会);这两段代码之间的接口必须有一种机制来传递进程失败的信息。有一些方法可以让这个接口实际上在延续函数本身内抛出异常(即:延续获取它调用的一些对象来访问当前读取的数据,如果发生读取错误,它就会抛出),但这仍然是一个 合作机制。

它不会自行发生。

因此,即使您可以在协程中解决此问题,您仍然需要考虑协程因内部抛出异常之外的原因而需要终止的情况。由于您将需要显式代码来执行清理/回滚/等操作无论如何,因此依靠纯粹的 RAII 机制来执行此操作毫无意义。

为了更直接地回答这个问题,如果您仍然想这样做,您需要将挂起点之间的代码视为它们自己的函数。每个挂起点实际上是一个单独的函数调用,具有自己的异常计数等等。

因此,要么 RAII 对象完全存在于挂起点之间,要么您需要在每次挂起点开始时更新异常计数。

【讨论】:

    猜你喜欢
    • 2019-12-01
    • 1970-01-01
    • 2021-07-30
    • 2014-04-17
    • 2015-05-12
    • 2012-09-12
    • 1970-01-01
    • 2011-07-10
    • 2016-03-01
    相关资源
    最近更新 更多