Per this latest C++ TS: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4628.pdf, and based on the understanding of C# async/await language support, I'm wondering what is the "execution context" (terminology borrowed from C#) of the C++ coroutines?
My simple test code in Visual C++ 2017 RC reveals that coroutines seem to always execute on a thread pool thread, and little control is given to the application developer on which threading context the coroutines could be executed - e.g. Could an application forces all the coroutines (with the compiler generated state machine code) to be executed on the main thread only, without involving any thread pool thread?
In C#, SynchronizationContext is a way to specify the "context" where all the coroutine "halves" (compiler generated state machine code) will be posted and executed, as illustrated in this post: https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/, while the current coroutine implementation in Visual C++ 2017 RC seems to always rely on the concurrency runtime, which by default executes the generated state machine code on a thread pool thread. Is there a similar concept of synchronization context that the user application can use to bind coroutine execution to a specific thread?
Also, what's the current default "scheduler" behavior of the coroutines as implemented in Visual C++ 2017 RC? i.e. 1) how a wait condition is exactly specified? and 2) when a wait condition is satisfied, who invokes the "bottom half" of the suspended coroutine?
My (naive) speculation regarding Task scheduling in C# is that C# "implements" the wait condition purely by task continuation - a wait condition is synthesized by a TaskCompletionSource owned task, and any code logic that needs to wait will be chained as a continuation to it, so if the wait condition is satisfied, e.g. if a full message is received from the low level network handler, it does TaskCompletionSource.SetValue, which transitions the underlying task to the completed state, effectively allowing the chained continuation logic to start execution (putting the task into the ready state/list from the previous created state) - In C++ coroutine, I'm speculating that std::future and std::promise would be used as similar mechanism (std::future being the task, while std::promise being the TaskCompletionSource, and the usage is surprisingly similar too!) - so does the C++ coroutine scheduler, if any, relies on some similar mechanism to carry out the behavior?
[EDIT]: after doing some further research, I was able to code a very simple yet very powerful abstraction called awaitable that supports single threaded and cooperative multitasking, and features a simple thread_local based scheduler, which can execute coroutines on the thread the root coroutine is started. The code can be found from this github repo: https://github.com/llint/Awaitable
Awaitable is composable in a way that it maintains correct invocation ordering at nested levels, and it features primitive yielding, timed wait, and setting ready from somewhere else, and very complex usage pattern can be derived from this (such as infinite looping coroutines that only get woken up when certain events happen), the programming model follows C# Task based async/await pattern closely. Please feel free to give your feedbacks.
The opposite!
C++ coroutine is all about control. the key point here is the
void await_suspend(std::experimental::coroutine_handle<> handle)
function.evey
co_await
expects awaitable type. in a nutshell, awaitable type is a type which provide these three functions:bool await_ready()
- should the program halt the execution of the coroutine?void await_suspend(handle)
- the program passes you a continuation context for that coroutine frame. if you activate the handle (for example, by callingoperator ()
that the handle provides - the current thread resumes the coroutine immediately).T await_resume()
- tells the thread which resumes the coroutine what to do when resuming the coroutine and what to return fromco_await
.so when you call
co_await
on awaitable type, the program asks the awaitable if the coroutine should be suspended (ifawait_ready
returns false) and if so - you get a coroutine handle in which you can do whatever you like.for example, you can pass the coroutine handle to a thread-pool. in this case a thread-pool thread will resume the coroutine.
you can pass the coroutine handle to a simple
std::thread
- your own create thread will resume the coroutine.you can attach the coroutine handle into a derived class of
OVERLAPPED
and resume the coroutine when the asynchronous IO finishes.as you can see - you can control where and when the coroutine is suspended and resumes - by managing the coroutine handle passed in
await_suspend
. there is no "default scheduler" - how you implement you awaitable type will decide how the coroutine is schedueled.So, what happens in VC++? unfortunately,
std::future
still doesn't havethen
function, so you can't pass the coroutine handle to astd::future
. if you await onstd::future
- the program will just open a new thread. look at the source code given by thefuture
header:So why did you see a win32 threadpool-thread if the coroutines are launched in a regular
std::thread
? that's because it wasn't the coroutine.std::async
calls behind the scenes toconcurrency::create_task
. aconcurrency::task
is launched under the win32 threadpool by default. after all, the whole purpose ofstd::async
is to launch the callable in another thread.