In this article, I’ll go over the top 5 threading mistakes I see in .NET applications and explain how to fix them. While threading is a complex topic with many different facets, these 5 mistakes represent the majority of beginner mistakes around threading.
I’m going to omit the mistake of “not using thread safety when you should” from this list, and instead focus on areas where you might accidentally make a mistake while attempting to handle threaded scenarios.
Synchronous Async/Await
.NET’s async
/ await
keywords make asynchronous programming significantly easier than it was in earlier iterations. However, this ease can make it tempting to propagate anti-patterns.
Sometimes code offers both a synchronous and an async implementation of an operation. Most developers would agree that an async approach is inherently better, but is it really?
Think about it this way: in an async operation, you need to spin up a new thread, wait for the thread to start, and then rejoin the original call. Additionally, that thread still needs to perform the same operation that the synchronous implementation does.
This means that async operations often cost more than their synchronous variants.
So what am I saying? That async code is bad?
Threading certainly isn’t bad, but it’s something you need to be intelligent about. Asynchronous operations can allow the user interface to be responsive during long-running operations or allow you to perform operations in parallel.
Take a look at the following code:
At first it might look like this is efficient, but what we’re actually saying here is:
- Run the
PlotDominationAsync
method and wait for it to complete - Run the
DeployTroopsAsync
method and wait for it to complete - Run the
EstablishEvilHeadquartersAsync
method and wait for it to complete - Run the
LaunchDeathRayAsync
method and wait for it to complete
Here we’re paying for the overhead of threading four times while still executing methods synchronously. In this specific example we get all of the penalties for threading with none of the benefits.
Don’t do this.
Instead, use the Task.WhenAll
method:
This will limit your wait operation to the longest running task and actually introduce benefits from async code.
Note that this example assumes that these four tasks can be run in any order.
ConfigureAwait
Take a look at the following snippet:
await ProgramKillerRobotsAsync
The await
keyword will by default wait for a task to complete and attempt to rejoin the SynchronizationContext
that originally spawned the task. This can vary by programming platform (WPF vs WinForms vs ASP .NET for example).
This might not sound too bad, but imagine a busy web server that handles a large number of requests per second. In this scenario, waiting for the original context can create artificial delays and even deadlocks in some scenarios.
To combat this, we use the ConfigureAwait
method on an awaitable operation like so:
await ProgramKillerRobotsAsync.ConfigureAwait(false)
Here we’re telling .NET that we don’t care what SynchronizationContext to use when resuming the operation, which is far more efficient in busy environments.
Of course, this is not a viable strategy in scenarios where you have certain special threads such as a user interface thread. In those cases, sometimes you do want to ensure that you only perform operations on the same thread. In this case, you can omit ConfigureAwait
or specify ConfigureAwait(true)
to retain the same threading preference.
Note: ASP .NET Core will by default not use a SynchronizationContext
, meaning that ConfigureAwait
doesn't impact its performance in one way or another. For .NET Framework applications, however, it is important.
Using the Wrong Collection
In scenarios where you have potentially multiple threads interacting with a collection, your choice of collection classes matter.
Specifically, you need to make a conscious decision on if you want your collection to be thread safe and, if so, how you want to accomplish that.
If you’re running in a scenario where multiple threads can manipulate the same collection, chances are that the collection should have a thread safety strategy around it.
Take a look at the following example:
Here it’s possible for the underlying collection to be changed between checking for a key’s presence and retrieving that value, which could result in threading-related bugs.
At the most manual level, you can introduce a lock object every time the collection is interacted with:
This is better, but now we’ve accepted a certain degree of responsibility and risk over our threaded code. We’re essentially promising that any time we work with our collection that we will remember to use the _myDataLock
and use it appropriately.
.NET gives us better tools than this.
In the concurrent collections namespace, we have a handful of thread-safe collections which simplify collection management in scenarios like this.
In the case of our example, we can use the ConcurrentDictionary class to handle multi-threading for us. Let’s take a look at the code:
As with anything else in technology, there are tradeoffs to using concurrent collections. Because these classes assume thread safety as their responsibility, if you use them in scenarios where you don’t need that thread safety, you will be paying for the safety overhead in terms of slower performance.
For this reason, do not always default to thread safe collections, but they are a tool in your toolkit for scenarios where you need them.
Static Classes / State
When working with static classes or singletons, thread safety becomes hard to manage.
If you are introducing some form of static state, you should plan on that state being accessed from multiple threads simultaneously at some point in the future.
Even if your main application uses threads infrequently, the presence of any sort of static state can wreak havoc on unit tests running in parallel, leading to inconsistent tests, hard to debug test failures, and tests that only fail when run in a certain order or alongside specific other tests.
In short, static state is a great way to lose hours of your time and wind up relying less on your unit tests at the same time.
My recommendation is to avoid static state whenever possible due to these concerns.
If this is somehow not possible, I recommend you use thread safe collections and lock statements as appropriate from the beginning, because if you don’t threading problems will arise later and it may take some time to track them down to the static state.
Threads work on the IL Level
Finally, let’s take a look at a common point of confusion with threads by starting with an example:
This is a very simple method. You wouldn’t think this would have issues, but imagine if you had 50 threads calling to the same method on an instance in parallel. Odds are that you’re not likely to wind up with DoggosPetted
being 50 at the end of that test run.
Why is this?
When we read code, we tend to think about things at a line by line or statement by statement basis. In this example we read this and think “Increase DoggosPetted
by 1″.
However, this is not what the Common Language Runtime (CLR) sees. The CLR sees statements akin to the following:
Read the value of DoggosPetted
Add 1 to the register
Do an add operation between 1 and the value read from DoggosPetted
Store the result of that operation in DoggosPetted
In a multi-threaded environment, if the CLR context switches to a different thread after the prior thread has read from DoggosPetted
but before it adds one and stores the new value back into DoggosPetted
, you will overwrite any adds that threads have accomplished in that time.
Since working with a lock
statement here would be a painful way of managing field state, .NET provides Interlocked
operations to perform atomic operations with shared state.
Let’s take a look and see what I mean:
Interlocked also provides methods to decrement, add, remove, and exchange integers, among other operations.
While talking about things at this level is a bit like getting into the weeds, it is important to understand that the way we read code and the way the CLR reads code is different and this can lead to problems in areas you don’t expect until you’ve been bitten by them.
Additional Resources
While these are the most common threading issues in .NET in my experience, this list is in no way exhaustive.
There are a lot of things that can go wrong with threading and many people who have dug in at an expert level to provide in-depth resources.
If you want to learn more about threading and potential mistakes, I highly recommend giving the Async Guidance page a view on GitHub.
The post .NET Threading Gotchas appeared first on Kill All Defects.
Top comments (10)
Nice Article, and very useful topic for alot of developers . . . so thanks for posting it.
However, I think that the first example confuses is #async with #threads a little too much.... somethign that will lead many deveopers to an incorrect understanding of what Async/Await is really for.
Based on the naming convention of 'Async' in the method names, it implies that these are I/O constrained methods and therefore the code sample is not correct.
It uses Task.Run() to wrap what are already Async methods that will always return a Task; in many cases this is incorrect. So this statement is only partially valid: "Think about it this way: in an async operation, you need to spin up a new thread, wait for the thread to start, and then rejoin the original call."
When working with Async/Await you MUST ALWAYS consider if the code logic is I/O constrained (e.g. depending on some external systems performance/response), or CPU constrained (e.g. depending on the internal CPU/Memory performance/limitations).
So if the method 'PlotDominationAsync' was in fact a REST API call (e.g. I/O constrained) then wrapping it in Task.Run() is a complete waste of a Thread from the thread pool, and an incorrect way to write this code. If however it was all computationally (e.g. CPU constrained) constrained, then yes spinning up another thread will allow multi-processor system to leverage another core to handle the work in parallel -- but with the side effect of consuming even more server resources; for example: potentially limiting the scalability of a web server.
Based on the naming convention of "Async" it implies that these are all I/O constrained so the correct code is this (which has NO additional thread overhead) and will 100% always be a better way to go because it will allow all 4 external systems to do their work in parallel, while actually RELEASING the current application thread to go do something else useful!
Thank you for your detailed writeup / response. I love posting things like this because the folks who have drilled deep can help me understand misconceptions I didn't even know I had.
I'll make the edit later today, but I appreciate you taking the time to respond.
Seriously the most complete and comprehensive article on the subject, including the feedback of rearea1616. It definitely clarify the concept! Thank you very much!
Thanks for the great article! However, I'd like to note that
async
is not about multithreading.Love this definition by Eric Lippert from SO:
Besides, I recommend to read Stephen Cleary's article on why there is no thread for pure
async
operation.Excellent article. I'm guilty of the Synchronous Async/Await section. I will use the Task.WhenAll method from now on. I have one question.
I've been taught in past companies that I worked on that you aways should use async/await all the way down from controller to database. Is it worth using async/await Even if the method does only one thing?
Stephen Cleary has series about
async
. Check out this article to find full answer for your question.TL;DR:
async
andawait
for natural, easy-to-read code.async
changes semantic of your code (e.g. there are some pitfalls with exceptions and stack traces).As for me, I almost always don't elide
async
because I had really hard-tracking issues because of elidingasync
several times. So, my rule is to elideasync
only when you should do it (e.g. in performance critical places).Good article Matt, very clear explaining you've put into this complex and hard topic of .NET. This is not only threading but asynchronous processing. I am amazed how many people do not understand the difference in those two concepts. Sthephen Cleary has got this all brilliantly covered, and Stephen Toub (on MSDN blogs) even more.
It is my understanding that ConfigureAwait(true) (the default) does not mean that it will resume on the same thread - just that the continuation will be handled by the same context as the one captured just prior to the call.
Context is a tricky term and I defer to Stephen Cleary for its definition:
It is up to the context to determine if a a new thread will be spun up, an existing thread extracted from a pool, or if the current thread will be reused.
In WPF and WinForms applications using the default (ConfigureAwait(true)) from the UI thread will ensure the continuation resumes on the main UI thread.
In ASP.NET Core and Console applications, the context is the thread-pool task scheduler. This means that if you set ConfigureAwait(true) in a Console application or ASP.NET Core application for example, you are not guaranteed to continue on the same thread because the boolean refers to resuming on the captured context which is not the same thing as a thread.
Thank you Matte. can you explain more in depth the ConfigureAwait?
Some comments may only be visible to logged-in visitors. Sign in to view all comments.