如果C++20协程既不调用也不存储完成处理程序,会发生什么?

66bbxpm5  于 2024-01-09  发布在  其他
关注(0)|答案(1)|浏览(264)

概述

当我使用BoostAsio的CompletionToken工具时,我设计从最终调用的CompletionToken生成completion_handler
但是我对completion_handler既没有被调用也没有被存储的行为感兴趣,在这种情况下,completion_handler只是被销毁了。
我已经使用use_awaitable CompletionToken测试了C++20协程的行为。然后我观察到有趣的行为。在completion_handler销毁路径执行co_await之后,堆栈被展开,ioc.run()int main()中完成。在这个过程中,没有捕获异常。这是预期的行为还是未定义的行为?如果是预期的行为,文档在哪里?我还没找到

代码

  1. #include <iostream>
  2. #include <boost/asio.hpp>
  3. namespace as = boost::asio;
  4. template <typename CompletionToken>
  5. auto func(bool call, CompletionToken&& token) {
  6. auto init =
  7. []
  8. (
  9. auto completion_handler,
  10. bool call
  11. ) {
  12. if (call) {
  13. std::move(completion_handler)();
  14. }
  15. else {
  16. // What is happend if completion_handler is neither invoked nor stored ?
  17. }
  18. };
  19. return as::async_initiate<
  20. CompletionToken,
  21. void()
  22. >(
  23. init,
  24. token,
  25. call
  26. );
  27. }
  28. struct trace {
  29. trace() { std::cout << __PRETTY_FUNCTION__ << std::endl; }
  30. ~trace() { std::cout << __PRETTY_FUNCTION__ << std::endl; }
  31. };
  32. as::awaitable<void> proc1() {
  33. trace t; // destructed correctly
  34. try {
  35. {
  36. std::cout << "before call=true" << std::endl;
  37. co_await func(true, as::use_awaitable);
  38. std::cout << "after call=true" << std::endl;
  39. }
  40. {
  41. std::cout << "before call=false" << std::endl;
  42. co_await func(false, as::use_awaitable);
  43. // the following part is never executed
  44. std::cout << "after call=false" << std::endl;
  45. }
  46. }
  47. catch (...) {
  48. std::cout << "caught exception" << std::endl;
  49. }
  50. std::cout << "co_return" << std::endl;
  51. co_return;
  52. }
  53. as::awaitable<void> proc2() {
  54. for (int i = 0; i != 2; ++i) {
  55. std::cout << "before proc1" << std::endl;
  56. co_await proc1();
  57. std::cout << "after proc1" << std::endl;
  58. }
  59. }
  60. int main() {
  61. as::io_context ioc;
  62. as::co_spawn(ioc.get_executor(), proc2, as::detached);
  63. ioc.run();
  64. std::cout << "finish" << std::endl;
  65. }

字符串

输出

  1. before proc1
  2. trace::trace()
  3. before call=true
  4. after call=true
  5. before call=false
  6. trace::~trace()
  7. finish


godbolt链接:https://godbolt.org/z/3dzoYban6

注意

在实践中,我不使用“might uninvokable”token,我使用基于错误代码的方法。

  1. template <typename CompletionToken>
  2. auto func(bool call, CompletionToken&& token) {
  3. auto init =
  4. []
  5. (
  6. auto completion_handler,
  7. bool call
  8. ) {
  9. if (call) {
  10. std::move(completion_handler)(boost::system::error_code{});
  11. }
  12. else {
  13. // invoke with error code
  14. std::move(completion_handler)(
  15. boost::system::errc::make_error_code(
  16. boost::system::errc::operation_canceled
  17. )
  18. );
  19. }
  20. };
  21. return as::async_initiate<
  22. CompletionToken,
  23. void(boost::syste::error_code const&)
  24. >(
  25. init,
  26. token,
  27. call
  28. );
  29. }

u4dcyp6a

u4dcyp6a1#

completion_handler类型为

  1. auto init = [](auto completion_handler, bool should_complete) {
  2. boost::asio::detail::awaitable_handler<boost::asio::any_io_executor> ch = std::move(completion_handler);
  3. if (should_complete) {
  4. std::move(ch)();
  5. } else {
  6. // What is happend if completion_handler is neither invoked nor stored ?
  7. }
  8. };

字符串
awaitable_handler间接继承自awaitable_thread,它在销毁时确实负责堆栈展开:

  1. // Clean up with a last ditch effort to ensure the thread is unwound within
  2. // the context of the executor.
  3. ~awaitable_thread()
  4. {
  5. if (bottom_of_stack_.valid())
  6. {
  7. // Coroutine "stack unwinding" must be performed through the executor.
  8. auto* bottom_frame = bottom_of_stack_.frame_;
  9. (post)(bottom_frame->u_.executor_,
  10. [a = std::move(bottom_of_stack_)]() mutable
  11. {
  12. (void)awaitable<awaitable_thread_entry_point, Executor>(
  13. std::move(a));
  14. });
  15. }
  16. }


通常,所有权从一个处理程序转移到另一个处理程序,但是在这里,没有任何东西再引用它,所以它不再存在。
虽然这是实现细节,但它使得如果科罗永远无法恢复,引用计数将变为零。此外,沿着有一些不错的代码注解,您可能会发现有见地,例如,impl/awaitable.hpp

  1. // An awaitable_thread represents a thread-of-execution that is composed of one
  2. // or more "stack frames", with each frame represented by an awaitable_frame.
  3. // All execution occurs in the context of the awaitable_thread's executor. An
  4. // awaitable_thread continues to "pump" the stack frames by repeatedly resuming
  5. // the top stack frame until the stack is empty, or until ownership of the
  6. // stack is transferred to another awaitable_thread object.
  7. //
  8. // +------------------------------------+
  9. // | top_of_stack_ |
  10. // | V
  11. // +--------------+---+ +-----------------+
  12. // | | | |
  13. // | awaitable_thread |<---------------------------+ awaitable_frame |
  14. // | | attached_thread_ | |
  15. // +--------------+---+ (Set only when +---+-------------+
  16. // | frames are being |
  17. // | actively pumped | caller_
  18. // | by a thread, and |
  19. // | then only for V
  20. // | the top frame.) +-----------------+
  21. // | | |
  22. // | | awaitable_frame |
  23. // | | |
  24. // | +---+-------------+
  25. // | |
  26. // | | caller_
  27. // | :
  28. // | :
  29. // | |
  30. // | V
  31. // | +-----------------+
  32. // | bottom_of_stack_ | |
  33. // +------------------------------->| awaitable_frame |
  34. // | |
  35. // +-----------------+

你呢?

我找到了一个更老的答案,坦纳·桑斯伯里解释了堆栈式协同程序的等效语义:
协程将被挂起,直到操作完成并调用完成处理程序,io_service被销毁,或Boost.Asio检测到协程已被挂起且无法恢复,此时Boost.Asio将销毁协程。

  • 从boost::asio::spawn做什么?*
展开查看全部

相关问题