An online markdown blog and knowledge repository.
Notes taken while working through Stephen Cleary's March 2013 article Async/Await - Best Practices in Asynchronous Programming.
Additional notes were taken while reading Task-based Asynchronous Programming Patterns from learn.microsoft.com.
There are some basic rules of thumb to follow:
These are a mix of good ideas, not necessarily hard-and-fast rules, as there are details to each one depending on the DotNET version including ASP.NET, WinForms, WPF, etc.
Async methods can return any of these three types:
Async Void should not be used except in the case of asynchronous Event Handlers:
SynchronizationContext
that was active when the handler was called, so they cannot be caught easily.Task.WhenAny
or Task.WhanAll
.Task
and Task<T>
methods.private
access which is more difficult to test and to compose with other methods.Async Task methods are preferred when there is no return Type expected:
Generic Async Task methods provide the most capability:
Task<MyCustomObject>
returns a MyCustomObject
instance.Task<T>
-return method is asynchronous.public async void ChildMethodAsync()
{
throw new InvalidOperationException();
}
public void ParentMethod()
{
try
{
ChildMethodAsync();
}
catch (Exception)
{
// will not get caught!
throw;
}
}
In order to see these Exceptions while debugging, use AppDomain.UnhandledException
(commonly available in WPF, ASP.NET).
A better approach:
// WPF event handler
private async void MyButton_OnClick(object sender, EventArgs e)
{
await MyCustomMethodAsync();
}
// apply async Task to the custom method
public async Task MyCustomMethodAsync()
{
// do awaitable work etc
await Task.Run(()=> ...);
}
This provides:
Do not "...provide an async implementation (or override) to a void-returning method on an interface (or base class)":
Action<T>
returns void, and is the delegate form of a void lambda, and so will also inherit all the same async void method issues."Lambdas should only be used if they're converted to a delegate type that returns Task (for example, Func<Task>
)." To do this, encapsulate a void-return lambda expression in an Action/<T>
delegate, similar to how a delegate
can encapsulate a named method.
See .net 8 Action<T> for how to utilize Action<T>
delegate.
There are consequences to doing this:
Task.Wait
or Task.Result
).Thread Pool SynchronizationContext:
SynchronizationContext
that does not allow thread selection outside of the calling "context".Main
method cannot by async else the Console App would complete before any code ran.Warning: Moving asynchronous code from a Console Application to a GUI or ASP.NET application will require editing the code to avoid deadlocks.
Task.Wait
or Task.Result
.When an Exception is thrown within an async (read: Task based) method:
Task.Wait
or Task.Result
: Exceptions are wrapped in AggregateException
and then thrown.AggregateException
and must be "unwrapped" to get each Exception (even if there is only 1).Exception
.Methods that "aren't fully asynchronous" will not return a completed Task.
Async code should not include synchronous, blocking code.
An example as written in Async/Await Best Practices in Asynchronous Programming
public static class NotFullyAsynchronousDemo
{
// This method synchronously blocks a thread
public static async Task TestNotFullyAsync()
{
// Yield is an immediate-return statement (an incomplete Task)
await Task.Yield();
// Upon returning the next line of code will block the GUI or ASP.NET request thread
Thread.Sleep(5000);
}
}
To Retrieve the result of a background task: Do not use Task.Wait
or Task.Result
and instead use await
To Wait for any Task to complete: Do not use Task.WaitAny
and instead use await Task.WhenAny
To Retrieve the results of multiple tasks: Do not use Task.WaitAll
and instead use await Task.WhenAll
To Wait a period of time: Do not use Thread.Sleep
and instead use await Task.Delay
Context is captured when an incomplete Task is awaited.
Captured context is used to resume the async method.
Resuming on the Context causes performance problems (slight, but they accumulate).
Advice: Await teh result of ConfigureAwait whenever possible.
ConfigureAwait(bool)
or ConfigureAwait(ConfigureAwaitOptions)
ConfigureAwaitOptions
:
await
on already completed Task to behave as if Task is not yet completed (forcing a Yield).await
of a Task that has ended in a Faulted or Canceled state.ConfigureAwait best practices:
ConfigureAwait()
Task requires additional code.As written in Async/Await Best Practices in Asynchronous Programming:
Task.Run
or TaskFacotry.StartNet
but not Task ctor
nor Task.Start()
.TaskFactory.FromAsync
or TaskCompletionSource<T>
.CancellationTokenSource
and CancellationToken
.IProgress<T>
and Progress<T>
.SemaphoreSlim
.AsyncLazy<T>
.AsyncCollection<T>
.About using ConfigureAwait
vs using just await
:
ConfigureAwait()
only makes sense when awaiting Tasks.await
acts on anything awaitable.Controller Actions:
Task
: A promise of eventual completion of an operation.
Task
requires less caching than an int32 Task
.Task.ContinueWith(delegate callback)
: The callback to call when the Task is completed.
Task
allocation issues in code generation.delegate callback
can be a lambda expression.ValueTask<TResult>
: A struct that can wrap a TResult
or a Task<TResult>
for return from an async method.
System.Threading.Tasks.Extensions
.ValueTask<TResult>
on Synchronous tasks.Task<TResult>
is only constructed in an Asynchronous return.ValueTask<TResult>
.IValueTaskSource<out TResult>
to support 'pooling' and reuse.ValueTask<TResult>
to wrap an IValueTaskSource<TResult>
to represent an Async operation and maintain high performance despite possibly high allocations using the DotNET 4-based Task
model.ValueTask
: Non-generic version of ValueTask<T>
:
void
.Hot Paths:
ValueTask<TResult>
is the expected return from Hot Paths in DotNET code.Should every new method return ValueTask
or ValueTask<TResult>
?
Task
and Task<TResult>
.Task
or Task<bool>
are the most performant awaitable return types.ValueTask<TResult>
is larger than Task<TResult>
so when every literal bit
counts, this matters.Under what situation is is better to choose ValueResult
over Task
(or their generic counterparts)?
This is a brain-dump of concepts and mid-level details from the Summary chapter:
Task
, Task<TResult>
, ValueTask
, ValueTask<TResult>
.Start
or Begin
to imply that the method will not return or throw the result of the async operation.Task
.Task<T>
.OUT
and REF
keywords in arguments list. Use a TResult
that is a Tuple
or custom Type instead.WhenAll
, WhenAny
.ContinueWith()
or the await
keyword.Enum
, provides information on Task lifecycle.Task.Start()
is called on them.Task.Start()
and will always assume a return Task
is in any state OTHER THAN enum:Created. Calling Task.Start()
on a returned Task
will cause InvalidOperationException
to be thrown.CancellationToken
argument.CancellationToken
instance for a cancellation request, and OPTIONALLY act on it to cancel its operation.Faulted
and RanToCompletion
.Wait()
and WaitAll()
will continue to run with an Exception.IProgress<T>
as an async method parameter. Generally used to update UI with Task status info.Progress
is determined by the consuming code.IProgress
implementation options include: Act upon only the latest update; Buffer all updates; Invoke an action at each update; Control whether update is marshalled to a particular Thread.IProgress<T>
implementing methods MUST allow IProgress to be Null w/o throwing.IProgres<T>
updates are SYNCHRONOUS (so that they are immediately reported).IProgress<T>
could utilize a Tuple with data such as: double:percentComplete, List<string>
:filesDiscovered; A Custom Type (suffix 'ProgressInfo') that encapsulates the API definition of progress.IProgress<T>
CTOR allows providing a single Event Handler, or MULTIPLE Event Handlers can be subscribed via the ProgressChanged
property.IProgress<T>
and CancellationToken
params are availabe in the async method, then up to 3 added overloads will be required beyond the base params list: MyMethodAsync(obj myParam); MyMethodAsync(CancellationToken cn, obj myParam); MyMethodAsync(IProgress<T>
progress, obj myParam); MyMethodAsync(CancellationToken cn, IProgress<T>
progress, obj myParam);CancellationToken
= None and IProgress<T>
= null can be used to reduce total overloads to only two.Generating TAP Methods can be done three ways:
System.Threading.Tasks.Task
or ...Task<TResult>
. Exceptions are packaged in the Task object and TaskStatus.Faulted state is set. OperationCanceledException
that are UNHANDLED results in TResult Task.Canceled state.System.Threading.Tasks
and System.Runtime.CompilerService
namespaces. Create a TaskCompletionSource<TResult>
object that calls SetResult when completed, or SetException when faulted, or SetCanceled if canceled by token (TrySetException, TrySetException, and TrySetCanceled, accordingly).System.Threading.Tasks.Task
object. This pattern can also be used to "return a cached Task".Example MANUAL generation of a TAP method:
// Manually generating a TAP method
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
{
// Quote: When you implement a TAP method manually
// you must complete the resulting task when the
// represented asynchronous operation completes.
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try
{
tcs.SetResult(stream.EndRead(ar));
}
catch (Exception exc)
{
tcs.SetException(exc);
}
}, state);
return tcs.Task;
}
Example HYBRID TAP method generation:
// Hybrid TAP method generation
public Task<int> MethodAsync(string input)
{
if (input == null) throw new ArgumentNullException("input");
return MethodAsyncInternal(input);
}
private async Task<int> MethodAsyncInternal(string input)
{
// code that uses await goes here
return value;
}
Includes asynchronous methods that are awaiting (a usually remote) process to return results, but not utilizing compute.
Ensure these are set to be asynchronous:
TaskCompletionSource<TResult>
type, which exposes Task property that returnsTask<TResult>
instance.TaskCompletionSource<TResult>
methods: SetResult()
, SetException()
, SetCanceled()
and their 'TrySet' variants.Includes asynchronous methods that are performing the computationally heavy processing.
Although these shouldn't be exposed publicly from the library, if they are they should be exposed as Synchronous (NOT async).
The implementation Library can decide whether to offload them to another thread or execute them in parallel.
Task.Run()
and TaskFactory.StartNew()
:
TaskFactory.StartNew()
() is well suited for fine-grained control over a Task with CancellationToken
, TaskCreationOptions
, and TaskScheduler
params list overloads.TaskFactory
, then Start()
the Task separately if needed.Task.ContinueWith()
:
CancellationToken
, continuation options, and a TaskScheduler.TaskFactory.ContinueWhenAll()
and TaskFactory.ContinueWhenAny()
:
In a Compute-bound Task:
CancellationToken
in the params list.CancellationToken
within the body of the method using token.ThrowIfCancellationRequested()
.Canceled
.Faulted
.Hybrid Workloads: Compute-bound and IO-bound:
CancellationToken
so if IO-bound task returns Cancelled
, following Task(s) are also returned in Cancelled
state.Task.ContinueWith()
.Task
: Returns void; await Task<TResult>
: returns TResult.Task
by using "continuation".TResult
is returned (if operation completed successfully). Canceled? OperationCanceledException
is thrown. Faulted state? Exception that caused the fault is re-thrown.Faulted
: Only a single Exception is propagated: AggregateException
, containing all actually thrown Exceptions
.If SynchronizationContext
object is associated with thread that executed async method when suspended:
SynchronizationContext.Current
property is not null: async method resumes in same context using context's Post()
method. Null? Relies on TaskScheduler
object that was current when method was suspended (usually the default TaskScheduler
on the Thread Pool). Resumption could be executed or scheduled.Task
in state RanToCompletion
.Configure Suspend/Resume with Yield
and ConfigureAwait
:
Task.Yield()
: Equivalent to asynchronously posting or scheduling back to the current Context.Task.ConfigureAwait()
: Current Context is captured when async method is suspended. This is used to invoke async method continuation point when resumed. Inform the await operation to NOT capture and resume on Context and instead continue wherever the async operation that was being awaited completed.// do not use this if it is important to return
// to a specific context like the UI thread
await someTask.ConfigureAwait(continueOnCapturedContext:false);
Canceling Async Operation:
CancellationTokenSource
to generate a CancellationToken
i.e. ConcellationTokenSource.Token
.Cancel()
to signal the CancellationToken
.Cancellation
can be requested FROM ANY THREAD.Cancellation.None
means "Can not be canceled by Token".Note: It is also possible for a Task to "self cancel" by calling the passed-in CancellationToken
and calling its Cancel()
method!
Monitoring Progress:
// consume this method by WPF to track download
// progress initiated by a button event handler
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
try
{
txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
new Progress<int>(p => pbDownloadProgress.Value = p));
}
finally { btnDownload.IsEnabled = true; }
}
Built-in Task-based Combinators:
Task.Run(Func<Task>())
: Shorthand for TaskFactory.StartNew()
. Good for offloading async work.Task.FromResult(arg)
: Use when data may already be available and just needs to be returned within a Task<TResult>
.Task.WhenAll()
: Wait on multiple async operations represented by Task
or TResult
. Multiple Overloads are available. Applies Paralellism to each started Task
. Exceptions propagate out of each Task and can be caught specifically (rather than by an AggregateException
object).Task.WhenAny()
: Await any single Task-based operation for Redundancy (compare 1st completion to others), Interleaving (process each completion as they complete), Throttling (Allow other Tasks to start as others complete - an extension of Interleaving), or Early Bailout (As soon as one task returns a cancellation or other 'signal', other incomplete Tasks get cancelled before completing).If a try-catch
block wraps an await Task.WhenAll()
operation, the Exceptions are consolidated into AggregateException
.
Unwrap AggregateException
by performing a foreach
in the catch block and iterate through Task
items selecting by Task.IsFaulted
and capture each Task Exception using Task.Exception
property.
Introduce pauses into async method execution:
Task.WhenAny()
.try
to capture WhenAny()
or WhenAll()
execution and a finally
bock to ensure any continuation code is executed.Caveats:
TaskFactory.ContinueWhenAll
and TaskFactory.ContinueWhenAny()
do not accept timeouts!Task.Wait()
, Task.WaitAll()
, and Task.WaitAny()
accept timeouts.Task.Delay()
and Task.WhenAny()
to implement a timeout.Several Combinators are included in the .NET Libraries, but custom Combinators can be designed and implemented:
RetryOnFault<T>(Func, maxRetries)
Task<T>
, accepts a Task<T>
, and calls return await function().ConfigureAwait(false)
In either of the examples in subsection RetryOnDefault, the custom methods attempt to return a function (awaited function using TAP) that might throw. A catch
statement tests the number of times the attempt has been made (1 to maxRetries) and will throw
on the last retry, else will iterate until last retry then return default(t)
.
Other ideas include:
Func<Task>
between retries to determine when to retry.If calling multiple APIs but a response from only 1 is needed, pick the 1st one to return and cancel the remaining calls, and return only the 1st completed call.
Overview (see the actual page for the code):
Func<T>
as an array of Cancellation Token and Task<T>
instances ([] functions
).function
in functions
and assign them to new Task[]
array.await Task.WhenAny(collectionOfFunctionInstances).ConfigureAwait(false)
and assign the result to a variable e.g. completed
.cancellationToken.Cancel
.Task
in tasks array and implement TaskContinuationOptions.OnlyOnFaulted
, assigning them to a variable e.g. ignored
.completed
variable (which is a Task
).WhenAny()
might introduce performance problems because it registers a Continuation with each Task.
Following a technique as explained in Interleaved Operations can help to avoid issues with performance and handling completed and/or failed tasks.
Note: Thread safety is important here and using the Interlocked class might be necessary to avoid preemptive overwriting of an instance variable (for example).
Another possible custom Combinator is Task<T> WhenAllOrFirstException(IEnumerable<Task<T>>)
:
Leverage Task
and Task<TResult>
with data structures in an async-compatible way.
AsyncCache:
The article demonstrates a custom Class of type TKey, TValue
(a dictionary or KVP), where an internal ConcurrentDictionary<Tkey, Lazy<Task<TValue>>>
maintains values registered via the CTOR, and accessed using an indexer
method. Accepting a Func<TKey, Task<TValue>> valueFactory
delegate ensures previously-added KVPs are rapidly found and a cahced copy can be returned using key
. Since this is built on top of System.Threading.Tasks.Task
it is capable of handling concurrent operations (multiple key access).
The Async Producer Consumer Collection:
An example as written in the Asunc Producer Consumer Collection (linked above):
public class AsyncProducerConsumerCollection<T>
{
private readonly Queue<T> m_collection = new Queue<T>();
private readonly Queue<TaskCompletionSource<T>> m_waiting =
new Queue<TaskCompletionSource<T>>();
public void Add(T item)
{
TaskCompletionSource<T> tcs = null;
lock (m_collection)
{
if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
else m_collection.Enqueue(item);
}
if (tcs != null) tcs.TrySetResult(item);
}
public Task<T> Take()
{
lock (m_collection)
{
if (m_collection.Count > 0)
{
return Task.FromResult(m_collection.Dequeue());
}
else
{
var tcs = new TaskCompletionSource<T>();
m_waiting.Enqueue(tcs);
return tcs.Task;
}
}
}
}
Now leverage the above code:
private static AsyncProducerConsumerCollection<int> m_data = …;
// additional fields, properties
private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.Take();
ProcessNextItem(nextItem);
}
}
// additional members
private static void Produce(int data)
{
m_data.Add(data);
}
With BufferBlock<T>
:
private static BufferBlock<int> m_data = ; // some value
// other fields and props...
private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.ReceiveAsync();
ProcessNextItem(nextItem);
}
}
// other members here
private static void Produce(int data)
{
m_data.Post(data);
}
This section contains notes gleaned from DevBlogs.Microsoft.com: Understanding the whys whats and whens of valuetask.
ValueTask
and ValueTask<TResult>
are:
Task
and Task<TResult>
.NEVER do the following with ValueTask
or ValueTask<TResult>
:
.GetAwaiter().GetResult()
when the operation state is NOT Completed
.Instead, use AsTask()
or simply await
or ConfigureAwait(false)
.
Using AsTask
:
.AsTask()
Task
or Task<TResult>
.ValueTask
or ValueTask<TResult>
again.Examples using await
, ConfigureAwait(false)
, and AsTask()
:
int result = await SomeValueTaskReturningMethodAsync();
int result = await SomeValueTaskReturningMethodAsync().ConfigureAwait(false);
Task<int> t = SomeValueTaskReturningMethodAsync().AsTask();
BENEFITS of using Task
and Task<TResult>
:
Completed
to Incomplete
state.GetAwaiter().GetResult()
and will block the caller (without risk of race condition) until Task completes.Quoted from Stephen Toub.
for()
loop example that Stephen Toub demonstrated (see Resources).Delegates in C# are 'managed function pointers'.
delegate
: A generic term for a parameterless, void-returning method.void Action<T>
: A specific, defined delegate that can be bound as a delegate
.(()=>{});
: Same as above. Can be bound to delegate
without having to identify Action<T>
(for example).TResult Func<T>
: A specific, defined delegate that returns something other than void.Consider Tasks to be Action<T>
delegates, but with super powers:
ContinueWith(Action action)
backed by a private Action? _continuation
field. The delegate is stored until the Task work IsCompleted
is true.lock
(potentially oversimplified) in the IsCompleted
getter, to ensure the completed
state is not changed by another thread: lock (this) { return _completed; }
Complete(Exception? exception)
method manages a potentially null _context
and returns the correct state using an ExceutionContext.Run()
implementation.Wait()
is an implementation of a 'Synchronization primitive' called ManualResetEventSlim?
, then enable signalling so that the process execution state is known to the parent class.public static MyTask Run(Action action){}
that leverages a ThreadPool mechanism to queue work item(s) for excution on a separate thread. This is exactly what Task.Run(delegate)
does! A try-catch
block is implemented to actually invoke the delegate.Note about lock
on this
: First off do not do it. A lock
is 'private state', but 'this' is public. This enables code to access private state. If no other code could get a handle on the lock (this)
code then it might be safe. However, a public
class that when instantiated basically ends up with a public this
, locking the private state is now accessible to other callers.
Key takeaway:
Task
and Task<T>
are ubiquitously important due to the value-add of managing threaded work execution.async-away
.Recall that an Exception instance can be thrown.
throw _exception
(when stored in a field).In .NET 4.0 the proper 'rethrow' implementation was to add the _exception
instance as an InnerException, so that all Watson Trace information is included.
// class and function code...
if (_exception is not null)
{
throw new Exception("comment", _exception); // e.g. (string command, Exception innerException)
}
// other code...
Tasks can represent multiple operations!
AggregateException
helps with this by allowing multiple Exceptions to be automatically wrapped without losing the Watson/StackTrace details (reducing code implementation by developers).
This is directly related to how Exceptions within Tasks are (and should be) handled:
ExceptionDispatchInfo
takes and throws an Exception using an append method. Exception 'state' is appended so that a Stack Trace returns all rethrown Exceptions with full fidelity.When Main() method exits, should it wait for all the Threads to complete before exiting?
Avoids the problem of using lock()
on a parallel set of operations:
Interlocked
has been available for the life of .NET Framework and .NET Core, .NET Standard, USP, and the latest .NET versions 6, 7, 8, and (apparently) 9!This affects the Thread Pool entirely!
await Task.Delay()
causes just that Thread to go do something else until it is signalled that it is time to process more Task work.Stephen Toub stated the following:
If cancellation is requested but a result or an exception is still produced, the task should end in the RanToCompletion
or Faulted
state.
A TAP method may even have nothing to execute, and may just return a Task that represents the occurrence of a condition elsewhere in the system (for example, a task that represents data arriving at a queued data structure).
The Producer-Consumer Collection pattern!
NuGet Package System.Threading.Tasks.Dataflow.
Asynchronout Programming Scenarios.
Async/Await - Best Practices in Asynchronous Programming
Stephen Cleary adds a great response to a StackOverflow question that is worth reviewing, especially in the context of (now legacy) ASP.NET.
Read more about the Task-based Asynchrounous Pattern TAP in .NET: Introduction and overview on MSDN.
MSFT DevBlogs Understanding The Whys Whats And Whens Of ValueTask.
Stephen Toub, Partner Software Engineer talks with Scott Hanselman about writing async-await from scratch in C#.
Return to Conted Index
Return to Root README