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).
-
AsyncLocalworks 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; //UnwrappingThe way around that is:
var task = Task.Factory.StartNew(async () => 5).Unwrap();
var result = await task; //Already unwrapped