I'm trying to implement coroutines using async/await, and for that I want to ensure my coroutines are only executing on one thread (the thread that resumes them).
I think you can safely rely upon this behavior. This should be true as long as you do not use any of the following features:
- Custom TPL task schedulers;
- Custom synchronization contexts;
ConfiguredTaskAwaitable
orTask.ConfigureAwait()
;YieldAwaitable
orTask.Yield()
;Task.ContinueWith()
;- Anything which may lead to a thread switch, like an async I/O API,
Task.Delay()
,Task.Run()
,Task.Factory.StartNew()
,ThreadPool.QueueUserWorkItem()
etc.
The only thing that you use here is TaskAwaiter
, more about it below.
First of all, you should not be worried about a thread switch inside the task, where you do await Coroutine.Yield()
. The code will be resumed exactly on the same thread where you explicitly call the continuation callback, you have complete control over this.
Secondly, the only Task
object you have here is that generated by the state machine logic (specifically, by AsyncTaskMethodBuilder
). This is the task returned by Test()
. As mentioned above, a thread switch inside this task may not take place, unless you do it explicitly before calling the continuation callback via your custom awaiter.
So, your only remaining concerned is about a thread switch which may happen at the point where you're awaiting the result of the task returned by Test()
. That's where TaskAwaiter
comes into play.
The behavior of TaskAwaiter
is undocumented. As far as I can tell from the Reference Sources, IsValidLocationForInlining
is not observed for the TaskContinuation
kind of continuation (created by TaskAwaiter
). The present behavior is the following: the continuation will be not inlined if the current thread was aborted or if the current thread's synchronization context is different from that captured by TaskAwaiter
.
If you don't want to rely upon this, you can create another custom awaiter to replace TaskAwaiter
for your coroutine tasks. You could implement it using Task.ContinueWith( TaskContinuationOptions.ExecuteSynchronously)
, which behavior is unofficially documented by Stephen Toub in his "When "“ExecuteSynchronously” doesn't execute synchronously" blog post. To sum up, the ExecuteSynchronously
continuation won't be inlined under the following severe conditions:
- the current thread was aborted;
- the current thread has stack overflows;
- the target task scheduler rejects inlining (but you're not using custom task schedulers; the default one always promotes inlining where possible).