Using async methods/lambdas where they are not expected causes unexpected problems. The typical example I discuss with people is TaskFactory.StartNew() because its an easy way to create Tasks and some people reach for that instead of Task.Run(), but I recently came across some code hitting the same problem while creating threads the traditional way.
In this scenario, the code was intended to create some child threads that poll a resource. That resource would sometimes be changed and the master thread would Abort() the child threads and create new ones polling the new resource. But there was a thread leak. Sometimes threads could continue to run after they should have been aborted.
A simplified example looks like this in a console application (I’m not creating the new threads after the abort because its not relevant to the bug in the logic).
We are simply starting two threads, waiting 1 second, and then aborting them. We assume they will stop and not output anything after the “Press ENTER” line. But notice that the two thread functions are asynchronous. What does that mean? As I’ve discussed before you can’t really make any assumptions about the internals of the async code you are calling. You can only know what you are doing yourself around the async operation. What we are doing here is “Start this asynchronous operation on another thread.” We don’t really know when this method will end, or even if it will end on our thread.
So when does our thread end? Can we even abort the operation this way? Not reliably, no.
If the code happens to look like this, we can abort it.
But that’s only because there is no asynchrony actually going on. This will run synchronously on the original thread that called it. But that’s pretty uncommon. More likely there is some asynchrony going on inside it like this
In this case there is an asynchronous delay. This means that all of the iterations of the loop after 0, will happen as asynchronous continuations. Where will they run? It depends on the context. In this example, a console application, they will run on a random thread pool thread.
That means that when I try to abort the thread I created, I’m just aborting the initial thread which is handling the first portion of the method. If the application gets to the await before it gets to the abort, the continuation will already scheduled and will go on oblivious to the fact that the original thread gets aborted. I can’t abort this method using Thread.Abort() because method isn’t just one thread.
Here is what the output looks like. You can see that until the abort happens (immediately before the “Press ENTER” text) the threads are pumping out even and odd numbers. After the abort, the PrintEvenNumbers method is happily continuing its way into infinity.
How to avoid this?
- Stick with Tasks and avoid raw Thread code if you can help it. The threading APIs predate the Task/Async APIs by a decade. Mixing the two can result in bugs like this one unless you are extra careful. Task.Run() is your friend for spawning/managing new Tasks, and it is async lambda friendly.
- Avoid Thread.Abort() like the plague. There are some unusual cases where it makes sense, but the vast majority of the times I see it, I consider it to be a bug. .NET provides a very clean cooperative cancellation mechanism that is simple and thread safe (the CancellationToken) which should be used instead to signal a child task to tear itself down cleanly and predictably.
- More on why async lambdas behave this way from the .NET Framework team
The sample code above is available here