Initially I thought that all continuations are executed on the threadpool (given a default synchronization context). This however doesn't seem to be the case when I use a TaskCompletionSource
.
My code looks something like this:
Task<int> Foo() {
_tcs = new TaskCompletionSource<int>();
return _tcs.Task;
}
async void Bar() {
Console.WriteLine(Thread.Current.ManagedThreadId);
Console.WriteLine($"{Thread.Current.ManagedThreadId} - {await Foo()}");
}
Bar
gets called on a specific thread and the TaskCompletionSource
stays unset for some time, meaning the returned tasks IsComplete = false
. Then after some time, the same thread would proceed to call _tcs.SetResult(x)
, which by my understanding should run the continuation on the threadpool.
But what I observed in my application is that the thread running the continuation is in fact still the same thread, as if the continuation was invoked synchronously right as SetResult
is called.
I even tried setting a breakpoint on the SetResult
and stepping over it (and having a breakpoint in the continuation), which in turn actually goes on to call the continuation synchronously.
When exactly does SetResult()
immediately call the continuation synchronously?
Initially I thought that all continuations are executed on the threadpool (given a default synchronization context). This however doesn't seem to be the case when I use a TaskCompletionSource.
Actually, when using await
, most continuations are executed synchronously.
Marc's answer is great; I just wanted to go into a bit more detail...
TaskCompletionSource<T>
by default will operate synchronously when Set*
is called. Set*
will complete the task and issue the continuations in a single method call. (This means that calling Set*
while holding a lock is a recipe for deadlocks.)
I use the weird phrase "issue the continuations" there because it may or may not actually execute them; more on that later.
The TaskCreationOptions.RunContinuationsAsynchronously
flag will tell TaskCompletionSource<T>
to issue the continuations asynchronously. This breaks apart the completing of the task (which is still done immediately by Set*
) from the issuing of the continuations (which is only triggered by the Set*
call). So with RunContinuationsAsynchronously
, a Set*
call will only complete the task; it will not execute the continuations synchronously. (This means that calling Set*
while holding a lock is safe.)
But back to the default case, which issues the continuations synchronously.
Each continuation also has a flag; by default a continuation is executed asynchronously, but it can be made synchronous by TaskContinuationOptions.ExecuteSynchronously
. (Note that await
does use this flag - link is to my blog; technically this is an implementation detail and not officially documented).
However, even if ExecuteSynchronously
is specified, there are a number of situations where the continuation is not executed synchronously:
TaskScheduler
associated with the continuation, that scheduler is given the option of rejecting the current thread, in which case the task is queued to that TaskScheduler
instead of executing synchronously.StackOverflowException
).That's quite a few conditions, but with your simple Console app test, they're all met:
TaskCompletionSource<T>
does not specify RunContinuationsAsynchronously
.await
) does specify ExecuteSynchronously
.TaskScheduler
specified.As a general rule, I would say any usage of TaskCompletionSource<T>
should specify TaskCreationOptions.RunContinuationsAsynchronously
. Personally, I think the semantics are more appropriate and less surprising with that flag.
SetResult
usually runs continuations from TCS synchronously. There main exception to this is if you explicitly pass in the TaskContinuationOptions.RunContinuationsAsynchronously
flag when creating the TCS (new in .NET 4.6). The other scenario when it runs things asynchronously is if it thinks the current thread is doomed.
This is quite important, because if you're not careful you can end up having calling code take control of a thread that was meant to be doing other work (like: dealing with socket IO).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With