跟随oldnewthing复习一下C++ Coroutine。这里有张Coroutine的学习路线图:A map through the three major coroutine series

第一部分: Awaitable Objects。

C++ coroutines: Getting started with awaitable objects

当await_suspend被调用的时候,对应的coroutine的handle就准备好了,可以随时恢复执行。而且在await_suspend返回之前,handle就可以被执行完毕。

如果在await_suspend中恢复执行handle,则在handle恢复时会执行相应的await_resume。

如果handle在await_suspend之中执行完毕,若提供await_suspend,await_resume的载体是临时变量,那么这个临时变量也会在await_suspend结束之前被释放。

在两个线程同时调用handle可能是一个未定义的行为

下面是在新线程执行handle的一个例子:

struct resume_new_thread : std::experimental::suspend_always
{
  void await_suspend(
      std::experimental::coroutine_handle<> handle)
  {
    std::thread([handle]{ handle(); }).detach();
  }
};

上面的代码也可以简化成:

  void await_suspend(
      std::experimental::coroutine_handle<> handle)
  {
    std::thread(handle).detach();
  }

detach的意思是在std::thread现例析构之后线程依然可以运行。

C++ coroutines: Constructible awaitable or function returning awaitable?

也从函数中返回一个awaitable:

auto resume_new_thread()
{
  struct awaiter : std::experimental::suspend_always
  {
    void await_suspend(
        std::experimental::coroutine_handle<> handle)
    {
      std::thread([handle]{ handle(); }).detach();
    }
  };
  return awaiter{};
}

从函数返回的一个好处是可以通过函数参数的变化返回不同的awaitable,并且函数的返回值可以指定[[nodiscard]]属性。

下列是一个对比图:

Property struct function
Instance members Yes Yes
Static members Yes No
Allows parameters Yes Yes
Different awaitable type depending on parameter types No Yes
Different awaitable type depending on parameter values No No
Warn if not co_awaited No Yes

C++ coroutines: Framework interop

coroutine_handle可以通过address()转化为void*地址,又可以通过from_address()转化回来。

the implicit conversion from a stateless lambda to a function pointer 解释了不捕捉状态的lambda可以转化成函数指针。下面是一个应用的例子:

struct resume_new_thread : std::experimental::suspend_always
{
  void await_suspend(
      std::experimental::coroutine_handle<> handle)
  {
    HANDLE thread = CreateThread(nullptr, 0,
      [](void* parameter) -> DWORD
      {
        std::experimental::coroutine_handle<>::
                    from_address(parameter)();
        return 0;
      }, handle.address(), 0, &threadId);
    if (!thread) throw some_kind_of_error();
    CloseHandle(thread);
  }
};

C++ coroutines: Awaiting an IAsyncAction without preserving thread context

IAsyncAction的默认awaiter会在原COM隔间恢复coroutine的执行。若不想要这个行为,可以自定义个awaiter。

C++ coroutines: Short-circuiting suspension, part 1

让await_suspend返回bool可以在一定程度上减少嵌套的调用。

C++ coroutines: Short-circuiting suspension, part 2

如果通过await_ready完全避免coroutine挂起。

C++ coroutines: no callable ‘await_resume’ function found for type

略。

C++ coroutines: Defining the co_await operator

如何从awaitable对象上取得一个awaiter呢?要么这个awaitable本身就是一个awaiter,具有await_suspend等方法,要么看看能不能将awaitable转化为awaiter。转化的一个办法是通过重载operator co_awaiter操作符。

查找operator co_awaiter的过程跟查找其他重载操作符类似,先看awaitable有没有成员函数,然后看有没有非成员的。最长使用的是后者,比如C++/WinRT定义了co_await(std::chrono::duration),可以让代码这样写:co_await 30s

C++ coroutines: The co_await operator and the function search algorithm

讲述自动查找operator co_awaiter失败后的一些办法。比如提供一个函数来供调用来返回awaiter,另外也可以提供使用前置命名空间来获取指向所需的operator co_awaiter

C++ coroutines: The problem of the synchronous apartment-changing callback

C++/WinRT使用IContext­Callback来记住awaiter所在的COM隔间:

void await_suspend(std::experimental::coroutine_handle<> handle)
{
    async.Completed([handle,
                     context = CaptureCurrentApartmentContext()]
                    (auto const&, Windows::Foundation::AsyncStatus)
    {
        // When the operation completes, get back to the
        // original apartment and resume the coroutine there.
        check_hresult(InvokeInContext(context.get(), handle));
    });
}

上面的问题是InvokeInContext是同步的,当一个线程InvokeInContext调用另一个线程的隔间中的执行决时,必须等待执行结束才能返回。

在UI场景中,为了避免阻塞UI线程,不应该让UI线程使用InvokeInContext来把任务交给后台线程。

在C++/WinRT中出现过的问题:

C++ coroutines: The problem of the DispatcherQueue task that runs too soon, part 1

下面是resumee_foreground的简化版:

auto resume_foreground(DispatcherQueue const& dispatcher)
{
  struct awaitable
  {
    DispatcherQueue m_dispatcher;
    bool m_queued = false;

    bool await_ready()
    {
      return false;
    }

    bool await_suspend(coroutine_handle<> handle)
    {
      m_queued = m_dispatcher.TryEnqueue([handle]
        {
          handle();
        });
      return m_queued;
    }

    bool await_resume()
    {
      return m_queued;
    }
  };
  return awaitable{ dispatcher };
}

上面代码的问题是await_resume可能在await_suspend执行,触发地点是在handle()调用,这时候m_queued还没有被赋值。当m_queue被赋值的时候,awaiter对象可能已经不存在了。

C++ coroutines: The problem of the DispatcherQueue task that runs too soon, part 2

采用一个非内核同步原语来实现await_suspend和await_resume之间的同步。

可以有三个地方放置这个同步对象:

  • 被入队的lambda内
    • 但是需要获取lambda内部变量的地址
  • await_suspend函数
    • 返回太快,可能await_suspend还没执行
  • 放在堆上,并用shared_ptr来引用
    • 额外的内存分配以及释放操作

作者给出的方案是让lambda等等await_suspend

    bool await_suspend(coroutine_handle<> handle)
    {
      slim_event* finder;
      bool result = m_dispatcher.TryEnqueue(
        [handle, tracker = slim_event_tracker(finder)] mutable
        {
          tracker.value.wait();
          handle()
        });

      m_queued = result;

      finder->value.signal();
      return result;
    }

C++ coroutines: The problem of the DispatcherQueue task that runs too soon, part 3

对上一篇中采取的方案的优缺点进行分析。缺点主要是采用了slim_event导致额外的损耗。

C++ coroutines: The problem of the DispatcherQueue task that runs too soon, part 4

对这个案例进行更深入分析,并注意到当lambda被执行的时候,就说明TryEnqueue成功这个事实。

所以await_suspend可以简化成:

    bool await_suspend(coroutine_handle<> handle)
    {
      // m_queued =
      return
        m_dispatcher.TryEnqueue([this, handle]
        {
          m_queued = true;
          handle();
        });
      // return m_queued;
    }

作者说到,写代码有时候需要停下来思考一下。

其他参考

(本部分完)