Configuring the continuation behaviour of a TaskCo

2020-03-24 08:37发布

问题:

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?

回答1:

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.



回答2:

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).



回答3:

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.