Search code examples
c#multithreadingasync-awaittask-parallel-librarytaskcompletionsource

How to substitute synchronization context / task scheduler to another one inside TaskCompletionSource.Task for ConfigureAwait(false)?


Assume I created a library containing such method:

Task MyLibraryMethodAsync()
{
    var taskCompletionSource = new TaskCompletionSource<object>();

    Action myWorkItem =
        () =>
        {
            // Simulate some work.
            // Actual work items depend on input params.
            Thread.Sleep(TimeSpan.FromSeconds(1));

            taskCompletionSource.SetResult(null);
        };

    // The next two lines is simplification for demonstration.
    // I do not have access to the workerThread - it is created
    // and managed for me by another lib.
    // All I can do - is to post some short work items to it.
    var workerThread = new Thread(new ThreadStart(myWorkItem));
    workerThread.Start();

    return taskCompletionSource.Task;
}

Any user of my lib can call MyLibraryMethodAsync like this

await MyLibraryMethodAsync().ConfigureAwait(false);
VeryLongRunningMethod();
void VeryLongRunningMethod()
{
    Thread.Sleep(TimeSpan.FromHours(1));
}

And here the problem comes – VeryLongRunningMethod will be executed inside the taskCompletionSource.SetResult(null) call and thus it will block workerThread for a long period of time, which is not desired behavior because workerThread is intended to run small portions of code (work items).

How do I substitute context / scheduler to a thread pool inside the returned task making await x.ConfigureAwait(false) to continue on the thread pool, but not on the workerThread?

The current solution I found is

Task MyLibraryMethodAsync()
{
    // ...

    return taskCompletionSource.Task
        .ContinueWith(x => x.Result, TaskScheduler.Default);
}

However, I do not like it due to overhead it creates. May be more elegant solution exists?


Solution

  • As of .NET 4.6 there is an option in TaskCreationOptions called RunContinuationsAsynchronously, which does exactly what you want, it ensures that all continuations are run asynchronously, rather than run synchronously when setting the result. TaskCompletionSource has an optional TaskCreationOption parameter in its constructor for you to provide that option.

    If you're using an earlier version of .NET you'll need to do a less efficient hack, such as adding another continuation, as you showed, or explicitly setting the result in a thread pool thread, rather than through the callback action.