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
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.
CancellationTokenSource
Always dispose CancellationTokenSource(s) used for timeouts
Cancelling operations
Using CancellationTokens
Good implementation should dispose CancellationTokenRegistration
.
Using a timeout
Good implementation cancels the timer if operation successfully completes.
Streams
Always call FlushAsync()
on StreamWriter
or Stream
before calling
Dispose()
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
- (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
-
awaited call will throw both synchronous and asynchronous exceptions
Eliding causes only synchronous exceptions to be caught:
-
Asynchronous and synchronous exceptions are normalized to always be asynchronous.
-
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
:
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:
The way around that is: