Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Configuring the continuation behaviour of a TaskCompletionSource's Task

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?

like image 206
Daniel C. Weber Avatar asked Oct 02 '12 15:10

Daniel C. Weber


1 Answers

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.

like image 73
Stephen Cleary Avatar answered Sep 27 '22 01:09

Stephen Cleary