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 exception return Something(); } public Task<string> FunctionWithElidinging() { throw Exception(); // synchronous exception return await Something(); } var task = FunctionWithoutEliding(); // throws some var result = await task; // Exception thrown here var task = FunctionWithElidinging(); // Exception thrown here var 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