DEV Community

loading...

Miscellaneous C# Async Tips

stuartblang profile image Stuart Lang Originally published at stu.dev on ・5 min read

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.Run(() => SomeBackgroundWork()); // Fire and forget

// becomes

_ = Task.Run(() => 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.Factory.StartNew(/* Something */).Unwrap()

to make it more familiar to other readers of the code, you can replace it with:

Task.Run(/* 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.Factory.StartNew(() =>
{
    while (true)
    {
        Thread.Sleep(1000); // Some blocking work you wouldn't want happening on your threadpool threads.
        DoSomething();
    }
}, TaskCreationOptions.LongRunning);

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<Task<string>> _lazyAccessToken = new Lazy<Task<string>>(async () => {
    return await RetrieveAccessTokenAsync();
});

public Task<string> AccessToken => _lazyAccessToken.Value;

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<Task<string>> _lazyAccessToken = new Lazy<Task<string>>(RetrieveAccessTokenAsync);

public Task<string> AccessToken => _lazyAccessToken.Value;

Note that it's important to defer the accessing of .Value.

We could encapsulate into a type like this:

internal sealed class AsyncLazy<T> : Lazy<Task<T>>
{
    public AsyncLazy(Func<Task<T>> taskFactory) :
    base(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 honourable mention goes to AsyncLazy defined in Nito.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
{
    public ApiClient()
    {
        AccessToken = RetrieveAccessTokenAsync();
    }

    public Task<string> AccessToken { get; }
    // here AccessToken is a hot Task, that may or may not be complete by the time it's first accessed

Async & 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 behaviour. 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 = new SemaphoreSlim(1,1) // initial: 1, max: 1  

async Task MyMethod(CancellationToken cancellationToken = default)
{
    await _semaphore.WaitAsync(cancellationToken);
    try
    {
        // the following line will only ever be called by one thread at any time
        await SomethingAsync();
    }
    finally
    {
        _semaphore.Release();
    }
}

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.

Discussion (4)

pic
Editor guide
Collapse
jeikabu profile image
jeikabu

I've had a massive c#/async post "in progress" for months now, but I doubt I'll ever finish it off. Some of the subleties surrounding the Slim variants aren't even in there, and makes me think that I'm not enough of a guru to contribute much. =)

Thanks for writing so much on Lazy, it's one technique that we didn't use as much as we should have...

Collapse
stuartblang profile image
Stuart Lang Author

Thanks! You should definitely try to finish your post, finishing can be the hardest part, but what you might assume is common knowledge will always help someone new 🙂

Collapse
vekzdran profile image
Vedran Mandić • Edited

Thanks for the writeup, very good (and quite important!) collection indeed. :-) Thanks for the SemaphorSlim hint (and the update comment on the danger of being locked on abortion).

Maybe worth mentioning, that it can get dangerous if one does not specify the factory method return type to be of Func but Action in the Lazy example. If so, one ends up getting an async void without even noticing it. Also one has to think really good if a Task.Run is truly needed? A dev should ask own self: "What is the true benefit of going to another thread (pool allocated) function if on await it'll wait anyway?" Of course, one should not confuse and match words block and wait. Blocking means a thread is idling and can not be reused elsewhere, while wait means the thread was released and some other thread is doing the other work. I guess that by mixing the semantics of these words is somehow the root of all problems with async / await. So one should also then ask own self: "Why not rather make it sync all the way if its only CPU bound action/operation?"

Luckily (or perhaps not for all, depends how you take it, i.e. what your code does) dotnet core now does not use a sync context as the old one (ASP.NET speaking, the UI frameworks still do as it does make more sense) did, so the new thread is reused back, and no context switching occurs, saving some resource, but opens the pit of "implicit parallelism" as S. Cleary states well in his article.

A wonderful collection on this topic if you are interested is provided by D. Fowler of course:

davidfowl / AspNetCoreDiagnosticScenarios

This repository has examples of broken patterns in ASP.NET Core applications

ASP.NET Core Diagnostic Scenarios

The goal of this repository is to show problematic application patterns for ASP.NET Core applications and a walk through on how to solve those issues. It shall serve as a collection of knowledge from real life application issues our customers have encountered.

Common Pitfalls writing scalable services in ASP.NET Core

Next you can find some guides for writing scalable services in ASP.NET Core. Some of the guidance is general purpose but will be explained through the lens of writing web services.

NOTE: The examples shown here are based on experiences with customer applications and issues found on Github and Stack Overflow.


P.S. sorry for the 101 async / await pep talk, but sometimes I feel (morally obliged) this has to be posted somewhere on the internet

Collapse
stuartblang profile image
Stuart Lang Author

You are absolutely right, I posted this because I felt the main topics have been discussed elsewhere and this was some bits and bobs I had that wasn't written anywhere.

The AspNetCoreDiagnosticScenarios repo you link to I happen to be (a minor) contributor to, and I have a full-length talk on being responsible with async (i/o bound vs thread bound) and about the consequences with being irresponsible with it: Async in C# - The Good, the Bad and the Ugly
by chance, I happen to cover all your points in that talk! Like I say "Luckily (or perhaps not for all, depends how you take it, i.e. what your code does)" almost word-for-word 😄