Consider the following code:
public Task SomeAsyncMethod()
{
var tcs = new TaskCompletionSource();
... do something, NOT setting the TaskCompletionSource...
return tcs.Task
}
public void Callsite1()
{
await SomeAsyncMethod();
Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
public void Callsite2()
{
SomeAsyncMethod().ContinueWith((task) =>
{
Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
});
}
At some point in time, the TaskCompletionSource created in SomeAsyncMethod is set on a ThreadPool Thread:
Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
tcs.SetResult();
When the Task from the TaskCompletionSource is awaited (as in Callsite1), the continuation executes synchronously on the Thread that called SetResult. When ContinueWith is called (as in Callsite2), the continuation executes asynchronously on a different thread.
It does not help to call configure the await, as in
public void Callsite1()
{
await SomeAsyncMethod().ConfigureAwait(true or false);
}
and this is not even the point of this question. As the implementor of SomeAsyncMethod, I do not want to call some potentially unknown code by calling SetResult. I want to have the continuation always scheduled asynchronously. And I cannot rely on the caller to configure the await properly (if that would even work).
How can a TaskCompletionSource be configured such that its Task doesn't execute its continuation synchronously when it's awaited?
There is no way to prevent synchronous task continuations. Normally, this is not a problem.
However, there are some situations where you do need this, e.g., if you are completing the task while holding a lock. In those cases, you can just Task.Run
the task completion, as such:
// Set the result on a threadpool thread, so any synchronous continuations
// will execute in the background.
Task.Run(() => tcs.TrySetResult(result));
// Wait for the TCS task to complete; note that the continuations
// may not be complete.
tcs.Task.Wait();
This is an advanced technique. It is an exception to the guideline "Don't block on async
code (async
all the way down)" as expounded on my blog.
It's part of my AsyncEx library, as an extension method:
public static void TrySetResultWithBackgroundContinuations<TResult>(
this TaskCompletionSource<TResult> @this, TResult result);
This technique was first published by Stephen Toub on his blog.
How can a TaskCompletionSource be configured such that its Task doesn't execute its continuation synchronously when it's awaited?
It can't be. You are exposing the Task
publicly, and once you do that anyone is free to attach a synchronous continuation (they just need to use another overload of ContinueWith
, they don't need to use async/await).
As of .NET 4.6, TaskCreationOption.RunContinuationsAsynchronously
was added for just this sort or case:
[It forces] continuations added to the current task to be executed asynchronously.
So, you can create a TaskCompletionSource
with TaskCreationOptions.RunContinuationsAsynchronously
and be sure that calling its .SetResult()
won't synchronously execute random code.
var tcs = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously);
// some time later, somewhere else
tcs.SetResult(true);
DoMoreStuffWithoutWorryingThatSetResultJustRanRandomCode();
More details are available in Stephen Toub's blog post "New Task APIs in .NET 4.6".
When a Task created with this option completes, it won't even try to invoke continuations synchronously... it'll simply invoke all of the continuations asynchronously as if none had asked to be executed synchronously if possible.
There's also TaskContinuationOptions.RunContinuationsAsynchronously
if you need similar behavior for things like .ContinueWith()
.
[TaskContinuationOptions.RunContinuationsAsynchronously
specifies] that the continuation task should be run asynchronously. This option has precedence over TaskContinuationOptions.ExecuteSynchronously.