There are lots of great async tips out there, here a random few that I’ve collected over the past couple of years that I haven’t seen much written about.
Use Discards with Task.Run for Fire and Forget
This is a minor bit of guidance, but one of my favourites. If you are starting a Task in a fire-and-forget manner with Task.Run, other readers of the code might be thinking “did they forget to await the returned task here?”.
You could make your intent explicit with a comment, but a very concise and clear way to indicate this was your intention is to use the discard feature in C# 7.0:
Task(class) TaskRepresents an asynchronous operation..Run(method) Task Task.Run(Action action)Queues the specified work to run on the thread pool and returns a Task object that represents that work.Parametersaction — The work to execute asynchronously.ReturnsA task that represents the work queued to execute in the ThreadPool.ExceptionsArgumentNullException — The action parameter was .(() =>(method) Task Task.Run(Action action)Queues the specified work to run on the thread pool and returns a Task object that represents that work.Parametersaction — The work to execute asynchronously.ReturnsA task that represents the work queued to execute in the ThreadPool.ExceptionsArgumentNullException — The action parameter was . SomeBackgroundWork(method) void SomeBackgroundWork()()); // Fire and forget
// becomes
_Task _ = Task(class) TaskRepresents an asynchronous operation..Run(method) Task Task.Run(Action action)Queues the specified work to run on the thread pool and returns a Task object that represents that work.Parametersaction — The work to execute asynchronously.ReturnsA task that represents the work queued to execute in the ThreadPool.ExceptionsArgumentNullException — The action parameter was .(() =>(method) Task Task.Run(Action action)Queues the specified work to run on the thread pool and returns a Task object that represents that work.Parametersaction — The work to execute asynchronously.ReturnsA task that represents the work queued to execute in the ThreadPool.ExceptionsArgumentNullException — The action parameter was . SomeBackgroundWork(method) void SomeBackgroundWork()());Of course comment if you need to clarify anything, but in this example I think it makes sense to omit the comment as it leaves less noise.
Replace Task.Factory.StartNew
Task.Factory.StartNew was often used before Task.Run was a thing, and StartNew can be a bit misleading when you are dealing with async code as it represents the initial synchronous part of an async delegate, due to their being no overload that takes a Func<Task<T>>.
Stephen Cleary even goes as far to say it is dangerous, and has very few valid use cases, including being able to start a LongRunning task for work that will block in order to prevent blocking on your precious thread pool threads. Bar Arnon points out this almost never mixes well with async code. In short StartNew is rarely useful, and importantly could cause confusion.
My first tip is, where ever you see the following:
Task(class) TaskRepresents an asynchronous operation..Factory(property) TaskFactory Task.FactoryProvides access to factory methods for creating and configuring Task and Task`1 instances.ReturnsA factory object that can create a variety of Task and Task`1 objects..StartNew(method) Task TaskFactory.StartNew(Action action)Creates and starts a task for the specified action delegate.Parametersaction — The action delegate to execute asynchronously.ReturnsThe started task.ExceptionsArgumentNullException — The action argument is .(/* Something */).Unwrap(method) <top-level-statements-entry-point>()to make it more familiar to other readers of the code, you can replace it with:
Task(class) TaskRepresents an asynchronous operation..Run(method) Task Task.Run(Action action)Queues the specified work to run on the thread pool and returns a Task object that represents that work.Parametersaction — The work to execute asynchronously.ReturnsA task that represents the work queued to execute in the ThreadPool.ExceptionsArgumentNullException — The action parameter was .(/* Something */)If you see usages without Unwrap(), there’s a chance it’s not doing what you think it’s doing. Or if you are using it to start a task with custom TaskCreationOptions, be sure it’s required for your scenario. To be clear, here is a rare scenario where it would make sense:
Task(class) TaskRepresents an asynchronous operation..Factory(property) TaskFactory Task.FactoryProvides access to factory methods for creating and configuring Task and Task`1 instances.ReturnsA factory object that can create a variety of Task and Task`1 objects..StartNew(method) Task TaskFactory.StartNew(Action action, TaskCreationOptions creationOptions)Creates and starts a task for the specified action delegate and creation options.Parametersaction — The action delegate to execute asynchronously.creationOptions — One of the enumeration values that controls the behavior of the created task.ReturnsThe started task.ExceptionsArgumentNullException — action is .ArgumentOutOfRangeException — creationOptions specifies an invalid TaskCreationOptions value.(() =>(method) Task TaskFactory.StartNew(Action action, TaskCreationOptions creationOptions)Creates and starts a task for the specified action delegate and creation options.Parametersaction — The action delegate to execute asynchronously.creationOptions — One of the enumeration values that controls the behavior of the created task.ReturnsThe started task.ExceptionsArgumentNullException — action is .ArgumentOutOfRangeException — creationOptions specifies an invalid TaskCreationOptions value.{ while (true) { Thread(class) ThreadCreates and controls a thread, sets its priority, and gets its status..Sleep(method) void Thread.Sleep(int millisecondsTimeout)Suspends the current thread for the specified number of milliseconds.ParametersmillisecondsTimeout — The number of milliseconds for which the thread is suspended. If the value of the millisecondsTimeout argument is zero, the thread relinquishes the remainder of its time slice to any thread of equal priority that is ready to run. If there are no other threads of equal priority that are ready to run, execution of the current thread is not suspended.ExceptionsArgumentOutOfRangeException — The time-out value is negative and is not equal to Infinite.(1000); // Some blocking work you wouldn't want happening on your threadpool threads. DoSomething(method) void DoSomething()(); }}, TaskCreationOptions(enum) TaskCreationOptionsSpecifies flags that control optional behavior for the creation and execution of tasks..LongRunning(constant) TaskCreationOptions.LongRunningSpecifies that a task will be a long-running, coarse-grained operation involving fewer, larger components than fine-grained systems. It provides a hint to the TaskScheduler that oversubscription may be warranted. Oversubscription lets you create more threads than the available number of hardware threads. It also provides a hint to the task scheduler that an additional thread might be required for the task so that it does not block the forward progress of other threads or work items on the local thread-pool queue.);Async & Lazy
One common place where people get caught out in migrating synchronous code to async is where they have lazy initiated properties, or singletons, and it now needs to be initialized asynchronously. A great tool in your toolbelt in the sync world to deal with this is Lazy<T>, however how can we use this with async and Tasks?
By default Lazy<T> uses the setting LazyThreadSafetyMode.ExecutionAndPublication, this means it’s thread-safe, and if multiple concurrent threads try to access the value, only one triggers the creation, and the others all wait for the value to become available.
What’s nice is this is kind of how Task<T> works with regards to awaiting. If one thread creates a Task instance that is awaited by other threads before it completes, they wait for the value to become available, without additionally executing the same work. However we need a thread-safe way to create the Task instance, so we can combine it with Lazy<T> like this:
readonly Lazy(class) Lazy<Task<string>>Provides support for lazy initialization.<(class) Lazy<Task<string>>Provides support for lazy initialization.Task(class) Task<string>Represents an asynchronous operation that can return a value.<(class) Task<string>Represents an asynchronous operation that can return a value.string(class) stringRepresents text as a sequence of UTF-16 code units.>(class) Task<string>Represents an asynchronous operation that can return a value.>(class) Lazy<Task<string>>Provides support for lazy initialization. _lazyAccessToken(field) Lazy<Task<string>> Example._lazyAccessToken = new Lazy(class) Lazy<Task<string>>Provides support for lazy initialization.<(class) Lazy<Task<string>>Provides support for lazy initialization.Task(class) Task<string>Represents an asynchronous operation that can return a value.<(class) Task<string>Represents an asynchronous operation that can return a value.string(class) stringRepresents text as a sequence of UTF-16 code units.>(class) Task<string>Represents an asynchronous operation that can return a value.>(class) Lazy<Task<string>>Provides support for lazy initialization.(async () =>(field) Lazy<Task<string>> Example._lazyAccessToken { return await RetrieveAccessTokenAsync(method) Task<string> Example.RetrieveAccessTokenAsync()();});
public Task(class) Task<string>Represents an asynchronous operation that can return a value.<(class) Task<string>Represents an asynchronous operation that can return a value.string(class) stringRepresents text as a sequence of UTF-16 code units.>(class) Task<string>Represents an asynchronous operation that can return a value. AccessToken(property) Task<string> Example.AccessToken =>(property) Task<string> Example.AccessToken _lazyAccessToken(field) Lazy<Task<string>> Example._lazyAccessToken.Value(property) Task<string> Lazy<Task<string>>.ValueGets the lazily initialized value of the current Lazy`1 instance.ReturnsThe lazily initialized value of the current Lazy`1 instance.ExceptionsMemberAccessException — The Lazy`1 instance is initialized to use the parameterless constructor of the type that is being lazily initialized, and permissions to access the constructor are missing.MissingMemberException — The Lazy`1 instance is initialized to use the parameterless constructor of the type that is being lazily initialized, and that type does not have a public, parameterless constructor.InvalidOperationException — The initialization function tries to access Value on this instance.;I’ve written this verbosely to be clear, but we can reduce this by making the statement a lambda, eliding the await, and replacing the invocation of RetrieveAccessTokenAsync with the method group like this:
readonly Lazy(class) Lazy<Task<string>>Provides support for lazy initialization.<(class) Lazy<Task<string>>Provides support for lazy initialization.Task(class) Task<string>Represents an asynchronous operation that can return a value.<(class) Task<string>Represents an asynchronous operation that can return a value.string(class) stringRepresents text as a sequence of UTF-16 code units.>(class) Task<string>Represents an asynchronous operation that can return a value.>(class) Lazy<Task<string>>Provides support for lazy initialization. _lazyAccessToken(field) Lazy<Task<string>> Example._lazyAccessToken = new Lazy(class) Lazy<Task<string>>Provides support for lazy initialization.<(class) Lazy<Task<string>>Provides support for lazy initialization.Task(class) Task<string>Represents an asynchronous operation that can return a value.<(class) Task<string>Represents an asynchronous operation that can return a value.string(class) stringRepresents text as a sequence of UTF-16 code units.>(class) Task<string>Represents an asynchronous operation that can return a value.>(class) Lazy<Task<string>>Provides support for lazy initialization.(RetrieveAccessTokenAsync(method) Task<string> Example.RetrieveAccessTokenAsync());
public Task(class) Task<string>Represents an asynchronous operation that can return a value.<(class) Task<string>Represents an asynchronous operation that can return a value.string(class) stringRepresents text as a sequence of UTF-16 code units.>(class) Task<string>Represents an asynchronous operation that can return a value. AccessToken(property) Task<string> Example.AccessToken =>(property) Task<string> Example.AccessToken _lazyAccessToken(field) Lazy<Task<string>> Example._lazyAccessToken.Value(property) Task<string> Lazy<Task<string>>.ValueGets the lazily initialized value of the current Lazy`1 instance.ReturnsThe lazily initialized value of the current Lazy`1 instance.ExceptionsMemberAccessException — The Lazy`1 instance is initialized to use the parameterless constructor of the type that is being lazily initialized, and permissions to access the constructor are missing.MissingMemberException — The Lazy`1 instance is initialized to use the parameterless constructor of the type that is being lazily initialized, and that type does not have a public, parameterless constructor.InvalidOperationException — The initialization function tries to access Value on this instance.;Note that it’s important to defer the accessing of .Value.
We could encapsulate into a type like this:
internal sealed class AsyncLazy(class) AsyncLazy<T><(class) AsyncLazy<T>TT>(class) AsyncLazy<T> :(class) AsyncLazy<T> Lazy(class) Lazy<Task<T>>Provides support for lazy initialization.<(class) Lazy<Task<T>>Provides support for lazy initialization.Task(class) Task<T>Represents an asynchronous operation that can return a value.<(class) Task<T>Represents an asynchronous operation that can return a value.TT>(class) Task<T>Represents an asynchronous operation that can return a value.>(class) Lazy<Task<T>>Provides support for lazy initialization.{ public AsyncLazy(method) AsyncLazy<T>.AsyncLazy(Func<Task<T>> taskFactory)(Func(delegate) Func<Task<T>>Encapsulates a method that has no parameters and returns a value of the type specified by the TResult parameter.ReturnsThe return value of the method that this delegate encapsulates.<(delegate) Func<Task<T>>Encapsulates a method that has no parameters and returns a value of the type specified by the TResult parameter.ReturnsThe return value of the method that this delegate encapsulates.Task(class) Task<T>Represents an asynchronous operation that can return a value.<(class) Task<T>Represents an asynchronous operation that can return a value.TT>(class) Task<T>Represents an asynchronous operation that can return a value.>(delegate) Func<Task<T>>Encapsulates a method that has no parameters and returns a value of the type specified by the TResult parameter.ReturnsThe return value of the method that this delegate encapsulates. taskFactory(parameter) Func<Task<T>> taskFactory) :(method) Lazy<Task<T>>.Lazy(Func<Task<T>> valueFactory)Initializes a new instance of the Lazy`1 class. When lazy initialization occurs, the specified initialization function is used.ParametersvalueFactory — The delegate that is invoked to produce the lazily initialized value when it is needed.ExceptionsArgumentNullException — valueFactory is . base(taskFactory(parameter) Func<Task<T>> taskFactory) { }}Or we could use the great vs-threading library from Microsoft (if you can get over the VisualStudio namespace), which contains lots of well polished async primitives such as AsyncLazy<T> you would almost expect to come included with the framework and part of .NET Standard.
Also, an honorable mention goes to AsyncLazyNito.AsyncEx.Coordination by Stephen Cleary.
An alternative strategy to AsyncLazy<T> if you don’t require it to be lazy, is to eagerly assign a field or property with a task in a constructor, which is started but not awaited, like this:
class ApiClient(class) Outer.ApiClient{ public ApiClient(method) Outer.ApiClient.ApiClient()() { AccessToken(property) Task<string> Outer.ApiClient.AccessToken = RetrieveAccessTokenAsync(method) Task<string> Outer.RetrieveAccessTokenAsync()(); }
public Task(class) Task<string>Represents an asynchronous operation that can return a value.<(class) Task<string>Represents an asynchronous operation that can return a value.string(class) stringRepresents text as a sequence of UTF-16 code units.>(class) Task<string>Represents an asynchronous operation that can return a value. AccessToken(property) Task<string> Outer.ApiClient.AccessToken { get; } // here AccessToken is a hot Task, that may or may not be complete by the time it's first accessedAsync & Lock
Another thing that trips people up when migrating their synchronous code is dealing with lock blocks, where async code cannot go. First I say, what are you trying to achieve? If it’s thread-safe initialization of a resource or singleton, then use Lazy/AsyncLazy mentioned above, your code will likely be cleaner and express your intent better.
If you do need a lock block which contains async code, try using a SemaphoreSlim. SemaphoreSlim does a bit more than Monitor and what you need for a lock block, but you can use it in a way that gives you the same behavior. By initializing the SemaphoreSlim with an initial value of 1, and a maximum value of 1, we can use it to get thread-safe gated access to some region of code that guarantees exclusive execution.
_semaphore(local variable) SemaphoreSlim _semaphore = new SemaphoreSlim(class) SemaphoreSlimRepresents a lightweight alternative to Semaphore that limits the number of threads that can access a resource or pool of resources concurrently.(1,1) // initial: 1, max: 1
async Task(class) TaskRepresents an asynchronous operation. MyMethod(method) Task MyMethod(CancellationToken cancellationToken = default(CancellationToken))(CancellationToken(struct) CancellationTokenPropagates notification that operations should be canceled. cancellationToken(parameter) CancellationToken cancellationToken = default(CancellationToken) = default){ await _semaphore(local variable) SemaphoreSlim _semaphore.WaitAsync(method) Task SemaphoreSlim.WaitAsync(CancellationToken cancellationToken)Asynchronously waits to enter the SemaphoreSlim, while observing a CancellationToken.ParameterscancellationToken — The CancellationToken token to observe.ReturnsA task that will complete when the semaphore has been entered.ExceptionsObjectDisposedException — The current instance has already been disposed.OperationCanceledException — cancellationToken was canceled.(cancellationToken(parameter) CancellationToken cancellationToken = default(CancellationToken)); try { // the following line will only ever be called by one thread at any time await SomethingAsync(method) Task SomethingAsync()(); } finally { _semaphore(local variable) SemaphoreSlim _semaphore.Release(method) int SemaphoreSlim.Release()Releases the SemaphoreSlim object once.ReturnsThe previous count of the SemaphoreSlim.ExceptionsObjectDisposedException — The current instance has already been disposed.SemaphoreFullException — The SemaphoreSlim has already reached its maximum size.(); }}What’s also cool is that SemaphoreSlim has a synchronous Wait method in addition to WaitAsync, so we can use it in scenarios where we need both synchronous and asynchronous access (be careful of course).
It may also be worth checking out AsyncLock from Nito.AsyncEx.Coordination.
That’s it for now, a bit of a random assortment I know.
Update: Thanks to Thomas Levesque for pointing out in the comments that SemaphoreSlim differs from Monitor/lock because it is not reentrant. This means a lock has no issue being acquired from a thread that has already acquired it, whereas with a SemaphoreSlim you would end up deadlocking yourself under the same scenario.
Another caveat feedback by odinserj in the comments is that on .NET Framework, a thread abort exception could leave the semaphore forever depleted, so it’s not bulletproof.
Comments