New Task APIs in .NET 4.6

Stephen Toub - MSFT

There are several nice API-level enhancements to the Task Parallel Library in .NET 4.6, which you can grab a preview of as part of the Visual Studio 2015 CTP.

Task.From*

.NET 4.5 had a Task.FromResult method.  This method makes it quick and easy to manufacture a new Task for a known result value, and is particularly useful when implementing a Task-returning method to complete synchronously.  However, Task didn’t expose corresponding methods for creating canceled or faulted tasks; instead, developers needed to manually create such tasks, such as by using a TaskCompletionSource, e.g.

public static Task<TResult> FromException<TResult>(Exception exc) {     var tcs = new TaskCompletionSource<TResult>();     tcs.SetException(exc);     return tcs.Task; }

Now in .NET 4.6, developers no longer need to write this boilerplate, as Task exposes several new static helpers, Task.FromCanceled and Task.FromException.  Imagine you were implementing a Stream-derived type whose Read method simply read and returned some data already in memory; it would likely be more efficient for ReadAsync to simply do the work synchronously rather than scheduling the memory reads to be done asynchronously.  Such functionality can now be done with fairly minimal boilerplate using these new APIs, e.g.

public class MyMemoryStream : Stream {     …     public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)     {         if (cancellationToken.IsCancellationRequested)             return Task.FromCanceled<int>(cancellationToken);         try         {             return Task.FromResult(Read(buffer, offset, count));         }         catch (Exception exc)         {             return Task.FromException(exc);         }     }     … }

Task.CompletedTask

In particular in library code that cares about performance and avoiding allocations, it’s frequently desired to implement fast-paths that, when possible, use cached already-completed tasks.  This is particularly true for a successfully-completed, non-generic Task.  Previously code that wanted such a cached task would often stash one in a static, e.g.

private static readonly Task s_completedTask = Task.FromResult(true); return s_completedTask; 

or it would rely on a method in the framework that the a developer knew (or hoped) would use such a cached task, e.g.

return Task.Delay(0);

With .NET 4.6, such a cached task has been exposed from the new Task.CompletedTask property.

return Task.CompletedTask;

Task{Creation/Continuation}Options.RunContinuationsAsynchronously

TaskCompletionSource<TResult> is very helpful in building Task-returning methods to wrap other asynchronous operations.  It’s also helpful in building asynchronous data structures and synchronization primitives.  I previously blogged about building such synchronization primitives, such as building an AsyncAutoResetEvent.  In that post, I noted that I had to be careful about where I invoked the TaskCompletionSource<TResult>.SetResult method:

“I talked about a ramification of calling {Try}Set* methods on TaskCompletionSource<TResult>, that any synchronous continuations off of the TaskCompletionSource<TResult>’s Task could run synchronously as part of the call.  If we were to invoke SetResult here while holding the lock, then synchronous continuations off of that Task would be run while holding the lock, and that could lead to very real problems.  So, while holding the lock we grab the TaskCompletionSource<bool> to be completed, but we don’t complete  it yet, delaying doing so until the lock has been released”

This is a not-uncommon issue that folks using TaskCompletionSource to build such types face and need to work around.  In this particular case, I avoided the cited problem by storing the TaskCompletionSource<TResult> into a local and then waiting until outside of the lock to call SetResult, so that synchronous continuations didn’t run on the thread calling SetResult while it held the lock.  That solved that particular problem, but there are others folks often need to work around as well.  For example, if you don’t want your code calling SetResult to have to wait for all synchronous continuations to complete before SetResult returns, you have to explicitly queue the call to SetResult so that the Task completion runs asynchronously from the caller and thus so do the continuations.  You can see an example of this in TPL Dataflow, with its RunCompletionAction method that takes a runAsync argument which will be set to true by callers that can’t afford to have continuations run synchronously as part of completing the task.

You can see another example of this in SemaphoreSlim.Release.  SemaphoreSlim.WaitAsync/Release are often used in async methods to provide mutual exclusion without blocking, e.g.

private SemaphoreSlim _gate = new SemaphoreSlim(1, 1); private async Task WorkAsync() {     await _gate.WaitAsync().ConfigureAwait(false);     try     {         … // work here     }     finally { _gate.Release(); } }

Now imagine that you have lots of calls to WorkAsync:

await Task.WhenAll(from i in Enumerable.Range(0, 10000) select WorkAsync());

We’ve just created 10,000 calls to WorkAsync that will be appropriately serialized on the semaphore.  One of the tasks will enter the critical region, and the others will queue up on the WaitAsync call, inside SemaphoreSlim effectively enqueueing the task to be completed when someone calls Release.  If Release completed that Task synchronously, then when the first task calls Release, it’ll synchronously start executing the second task, and when it calls Release, it’ll synchronously start executing the third task, and so on.  If the “//work here” section of code above didn’t include any awaits that yielded, then we’re potentially going to stack dive here and eventually potentially blow out the stack.  In large part for this reason, SemaphoreSlim.Release doesn’t actually complete the task synchronously; rather, Release queues the completion of the task so that it happens asynchronously from the caller.

To make these kinds of issues easier to address, and also to make them a bit more obvious, in .NET 4.6 the TaskCreationOptions and TaskContinuationOptions enums include a new value: RunContinuationsAsynchronously.  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 are already multiple reasons why a continuation that requested synchronous execution may not run synchronously, e.g. if the TaskScheduler denies it, and now the developer creating the task can explicitly deny such requests.

TaskCompletionSource<TResult> has a constructor that takes a TaskCreationOptions value, so this option can easily be used with tasks created by TaskCompletionSource<TResult>.  Returning to the aforementioned AsyncAutoResetEvent as an example, we can tweak its implementation to use this new option:

public Task WaitAsync() {     lock (m_waits)     {         if (m_signaled)         {             m_signaled = false;             return s_completed;         }         else         {             var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);             m_waits.Enqueue(tcs);             return tcs.Task;         }     } }

public void Set() {     lock (m_waits)     {         if (m_waits.Count > 0)             m_waits.Dequeue().SetResult(true);         else if (!m_signaled)             m_signaled = true;     } }

Enjoy!

0 comments

Discussion is closed.

Feedback usabilla icon