TAP Tips
TAP stands for Task-based Asynchronous Pattern. Here’re a few tips how to use it effectively in some scenarios.
Long running operations
Don’t use Task.Run
for long running tasks. Task.Factory.StartNew
has an
option TaskCreationOptions.LongRunning
that under the covers creates a new
thread and returns a Task that represents the execution. Using this properly
requires several non-obvious parameters to be passed in to get the right
behavior on all platforms.
Don’t use TaskCreationOptions.LongRunning
with async code as this will create
a new thread which will be destroyed after first await
public void StartProcessing(){ var thread = new Thread(ProcessQueue) { // This is important as it allows the process to exit while this thread is running IsBackground = true }; thread.Start();}
TaskCompletionSource
Always create TaskCompletionSource<T>
with
TaskCreationOptions.RunContinuationsAsynchronously
. By default, Task
continuations will run inline on the same thread that calls
Try/Set(Result/Exception/Canceled). As a library author, this means having to
understand that calling code can resume directly on your thread. This is
extremely dangerous and can result in deadlocks, thread-pool starvation,
corruption of state (if code runs unexpectedly) and more.
var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
CancellationTokenSource
Always dispose CancellationTokenSource(s) used for timeouts
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))
Cancelling operations
Using CancellationTokens
Good implementation should dispose CancellationTokenRegistration
.
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken){ var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
// This disposes the registration as soon as one of the tasks trigger using (cancellationToken.Register(state => { ((TaskCompletionSource<object>)state).TrySetResult(null); }, tcs)) { var resultTask = await Task.WhenAny(task, tcs.Task); if (resultTask == tcs.Task) { // Operation cancelled throw new OperationCanceledException(cancellationToken); }
return await task; }}
Using a timeout
Good implementation cancels the timer if operation successfully completes.
public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout){ using (var cts = new CancellationTokenSource()) { var delayTask = Task.Delay(timeout, cts.Token);
var resultTask = await Task.WhenAny(task, delayTask); if (resultTask == delayTask) { // Operation cancelled throw new OperationCanceledException(); } else { // Cancel the timer task so that it does not fire cts.Cancel(); }
return await task; }}
Streams
Always call FlushAsync()
on StreamWriter
or Stream
before calling
Dispose()
using (var streamWriter = new StreamWriter(context.Response.Body)){ await streamWriter.WriteAsync("Hello World"); // Force an asynchronous flush await streamWriter.FlushAsync();}
Async Eliding
In some cases, we might be able to write methods that return a Task
, without
any await
keywords. Such approach comes with some benefts and drawbacks.
Pros of eliding async
public Task<int> DoSomethingAsync(){ return InternalAsync();}
- (minimal) performance gain - without
async
-await
, the state machine does not need to be generated. An application will execute faster abd it will allocate less memory.
Pros of NOT eliding async
public async Task<int> DoSomethingAsync(){ return await InternalAsync();}
-
awaited call will throw both synchronous and asynchronous exceptions
Eliding causes only synchronous exceptions to be caught:
public Task<int> DoSomethingAsync(){try{return InternalAsync();}catch(Exception){// It will catch only synchronous exceptions, anything// asynchronous will not be caught here, but rather// on the caller of DoSomethingAsync (unless it's not awaited)}} -
Asynchronous and synchronous exceptions are normalized to always be asynchronous.
public async Task<string> FunctionWithoutEliding(){throw Exception(); // synchronous exceptionreturn Something();}public Task<string> FunctionWithElidinging(){throw Exception(); // synchronous exceptionreturn await Something();}var task = FunctionWithoutEliding(); // throws somevar result = await task; // Exception thrown herevar task = FunctionWithElidinging(); // Exception thrown herevar result = await task; -
Diagnostics of asynchronous methods is easier (full call stack).
-
AsyncLocal
works as expected (stephencleary.com)
Attached/Detached tasks
Task.Run
uses Task.Factory.StartNew()
under the hood with an option
DenyChildAttach
. It means that it will ignore children tasks being attached.
If we want the attachment, we can use Task.Factory.StartNew
:
await Task.Factory.StartNew(() -> { Task.Factory.StartNew(() -> { Thread.Sleep(1000); }, TaskCreationoptions.AttachedToParent);
Task.Factory.StartNew(() -> { Thread.Sleep(1000); }, TaskCreationoptions.AttachedToParent);}, TaskCreationOptions.);
The parent Task
will be completed only when its 2 children are finished.
Task.Run
would complete before that, because it does not allow attachment.
Unwrapping
Task.Run
automatically unwraps the result of async operations inside.
Task.Factory.StartNew
does not do that. Example:
var task = Task.Factory.StartNew(async () => 5);
var result = await await task; //Unwrapping
The way around that is:
var task = Task.Factory.StartNew(async () => 5).Unwrap();
var result = await task; //Already unwrapped