跟随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使用IContextCallback来记住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中出现过的问题:
- IContextCallback blocks coroutine’s thread of execution indefinitely #544
- Improve scalability of C++/WinRT’s coroutine resumption #546
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;
}
作者说到,写代码有时候需要停下来思考一下。
其他参考
- C++ links
- C++ links: Coroutines
- My tutorial and take on C++20 coroutines
- C++ coroutines: The mental model for coroutine promises
- “a coroutine’s promise must declare either ‘return_value’ or ‘return_void’” Error Visual Studio 2019 C++ 20
- C++ coroutines: Accepting types via return_void and return_value
- Subject: Re: [std-proposals] defect report: coexisting return_value and return_void in coroutines
(本部分完)