Who among us does not mow? I regularly meet with errors in asynchronous code and make them myself. To stop this wheel of the Sansara, I share with you the most typical shoals of those that are sometimes quite difficult to catch and repair.
This text is inspired by Stephen Clary 's blog , a person who knows everything about competitiveness, asynchrony, multithreading and other scary words. He is the author of the book Concurrency in C # Cookbook , which has collected a huge number of patterns for working with competitiveness.
To understand the asynchronous deadlock, it is worth understanding which thread the method called using the await keyword is executed on.
First, the method will go deeper into the chain of calls to async methods until it encounters a source of asynchrony. How exactly the asynchronous source is implemented is a topic that goes beyond the scope of this article. Now, for simplicity, we assume that this is an operation that does not require a workflow while waiting for its result, for example, a database request or an HTTP request. The synchronous launch of such an operation means that while waiting for its result in the system there will be at least one falling asleep thread that consumes resources but does not perform any useful work.
In an asynchronous call, we kind of break the command flow to “before” and “after” the asynchronous operation and in .NET there are no guarantees that the code lying after await will be executed in the same thread as the code before await. In most cases, this is not necessary, but what to do when this behavior is vital for the program to work? It is necessary to use SynchronizationContext
. This is a mechanism that allows you to impose certain restrictions on the threads in which the code is executed. Next, we will deal with two synchronization contexts ( AspNetSynchronizationContext
and AspNetSynchronizationContext
), but Alex Davies writes in his book that there are about a dozen in .NET. About SynchronizationContext
well written here , here , and here the author has implemented his own, for which he has great respect.
So, as soon as the code comes to the asynchrony source, it saves the synchronization context that was in the SynchronizationContext.Current
property of the thread-static, then it starts the asynchronous operation and releases the current thread. In other words, while we wait for the completion of an asynchronous operation, we do not block any thread and this is the main profit from an asynchronous operation compared to a synchronous one. After the completion of the asynchronous operation, we must follow the instructions that are after the asynchronous source and here, in order to decide in which thread we execute the code after the asynchronous operation, we need to consult with the previously saved synchronization context. As he says, so we will do. He will say to execute in the same thread as the code before await - we will execute in the same, it will not say - we will take the first available stream from the pool.
And what to do if in this particular case it is important for us that the code after await is executed in any free stream from the thread pool? You need to use the ConfigureAwait(false)
mantra ConfigureAwait(false)
. The false value passed to the continueOnCapturedContext
parameter just informs the system that any stream from the pool can be used. And what will happen if at the moment of executing the method with await there was no synchronization context at all ( SynchronizationContext.Current == null
), as for example in a console application. In this case, we have no restrictions on the thread in which the code should be executed after await and the system will take the first available stream from the pool, as is the case with ConfigureAwait(false)
.
So, what is asynchronous deadlock?
The difference between WPF and WinForms applications is the presence of the synchronization context itself. The WPF and WinForms synchronization context has a special stream - the user interface stream. The UI stream is one per SynchronizationContext
and only from this stream can you interact with user interface elements. By default, the code that started working in the UI thread resumes operation after an asynchronous operation in it.
private void Button_Click(object sender, System.Windows.RoutedEventArgs e) { StartWork().Wait(); } private async Task StartWork() { await Task.Delay(100); var s = "Just to illustrate the instruction following await"; }
StartWork().Wait()
:StartWork
method and StartWork
to the await Task.Delay(100)
instruction.Task.Delay(100)
operation, and will return control to the Button_Click
method, and there it will be the Wait()
method of the Task
class. When the Wait()
method is called, the UI thread is blocked until the end of the asynchronous operation, and we expect that as soon as it completes, the UI thread will immediately pick up the execution and go further along the code, however, everything will be wrong.Task.Delay(100)
completes, the UI thread must first continue with the StartWork()
method and for this it needs the exact thread in which the execution started. But the UI thread is currently busy waiting for the result of the operation.StartWork()
: StartWork()
cannot continue execution and return the result, and Button_Click
is waiting for the same result, and because the execution started in the user interface thread, the application simply hangs without a chance to continue working.Task.Delay(100)
call to Task.Delay(100).ConfigureAwait(false)
: private void Button_Click(object sender, System.Windows.RoutedEventArgs e) { StartWork().Wait(); } private async Task StartWork() { await Task.Delay(100).ConfigureAwait(false); var s = "Just to illustrate the instruction following await"; }
This code will work without deadlocks, since now the thread from the pool can be used to complete the StartWork()
method, and not the blocked UI thread. Stephen Clary recommends using ConfigureAwait(false)
in all “library methods” in his blog, but specifically emphasizes that using ConfigureAwait(false)
to treat deadlocks is a wrong practice. Instead, he advises NOT to use blocking methods like Wait()
, Result
, GetAwaiter().GetResult()
and translate all methods to use async / await, if possible (the so-called Async principle all the way).
ASP.NET also has a synchronization context, but it has some other limitations. It permits the use of only one thread per request at the same time and also requires that the code after await be executed in the same thread as the code before await.
public class HomeController : Controller { public ActionResult Deadlock() { StartWork().Wait(); return View(); } private async Task StartWork() { await Task.Delay(100); var s = "Just to illustrate the code following await"; } }
This code will also cause deadlock, because at the time of the StartWork().Wait()
call, the only allowed thread will be blocked and will wait for the StartWork()
operation to StartWork()
, and it will never end, since the thread that needs to be continued is busy waiting
It is fixed all the same ConfigureAwait(false)
.
Now let's try to run the code from the example for ASP.NET in the project for ASP.NET Core. If we do this, we will see that there will be no deadlock. This is due to the fact that there is no synchronization context in ASP.NET Core. Fine! And what, now you can cover the code with blocking calls and not be afraid of deadlocks? Strictly speaking, yes, but remember that this causes the stream to fall asleep while waiting, that is, the stream consumes resources, but does not perform any useful work.
Remember that using blocking calls eliminates all the benefits of asynchronous programming, making it synchronous . Yes, sometimes without using Wait()
it will not be possible to write a program, but the reason must be serious.
The Task.Run()
method was created to start operations in a new thread. As befits a method written using a TAP pattern, it returns Task
or Task<T>
and for people who first encounter async / await there is a great desire to wrap the synchronous code in Task.Run()
and retrieve the result of this method. The code seemed to become asynchronous, but in fact nothing has changed. Let's see what happens with this use of Task.Run()
.
private static async Task ExecuteOperation() { Console.WriteLine($"Before: {Thread.CurrentThread.ManagedThreadId}"); await Task.Run(() => { Console.WriteLine($"Inside before sleep: {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Console.WriteLine($"Inside after sleep: {Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine($"After: {Thread.CurrentThread.ManagedThreadId}"); }
Before: 1 Inside before sleep: 3 Inside after sleep: 3 After: 3
Here Thread.Sleep(1000)
is any synchronous operation that requires a thread to perform. Suppose we want to make our solution asynchronous, and so that this operation can be done, we wrapped it in Task.Run()
.
As soon as the code reaches the Task.Run()
method, another thread gets from the thread pool and executes the code we passed to Task.Run()
. The old stream, as it should be for a decent stream, returns to the pool and waits for him to be called to do the work again. The new thread executes the transferred code, reaches the synchronous operation, synchronously executes it (waits until the operation is completed) and proceeds along the code. In other words, the operation remained synchronous: we, as before, use the stream during the execution of a synchronous operation. The only difference is that we spent time switching the context when we called Task.Run()
and when we returned to ExecuteOperation()
. Things got a little worse.
It should be understood that despite the fact that in the lines Inside after sleep: 3
and After: 3
we see the same flow Id, in these places there is a completely different execution context. Just ASP.NET smarter than us and tries to save resources when switching context from the code inside Task.Run()
to external code. Here he decided not to change at least the flow of execution.
In such cases, there is no point in using Task.Run()
. Instead, Clary advises making all operations asynchronous, that is, in our case, replace Thread.Sleep(1000)
with Task.Delay(1000)
, but this, of course, is not always possible. What to do in cases when we use third-party libraries that we cannot or do not want to rewrite and make asynchronous until the end, but for one reason or another we need the async method? It is better to use Task.FromResult()
to wrap the result of the work of vendor methods in Task. This, of course, does not make the code asynchronous, but at least we will save on context switching.
Why then use Task.Run ()? The answer is simple: for CPU-bound operations, when you need to maintain responsiveness of the UI or parallelize the calculations. Here it must be said that CPU-bound operations are synchronous in nature. Task.Run()
was invented to start synchronous operations in asynchronous style.
void
was added to write asynchronous event handlers. Let's see why they can make confusion, if they are used for other purposes:Task.WhenAll()
, Task.WhenAny()
and other similar methods.Of all the reasons listed, the most interesting point is the exception handling. The fact is that in the async methods that return Task
or Task<T>
, exceptions are caught and wrapped in a Task
object, which will then be passed to the calling method. In his MSDN article, Clary writes that since there are no return values ​​in the async-void methods, there’s no way to wrap exceptions in the context of synchronization. The result is an unhandled exception due to which the process crashes, succeeding, except to write an error to the console. You can get and log such exceptions by subscribing to the AppDomain.UnhandledException
event, but the process crash cannot be stopped even in the event handler. This behavior is typical just for the event handler, but not for the usual method, from which we expect the possibility of standard exception handling via try-catch.
public IActionResult ThrowInAsyncVoid() { ThrowAsynchronously(); return View(); } private async void ThrowAsynchronously() { throw new Exception("Obviously, something happened"); }
But it is worth changing the type of the return value of the ThrowAsynchronously
method to Task
(even without adding the await keyword) and the exception will be intercepted by the standard ASP.NET Core error handler, and the process will continue to live despite the exception.
Be careful with async-void methods - they can put you in the process.
The latest anti-pattern is not as terrible as the previous ones. The bottom line is that it makes no sense to use async / await in methods that, for example, simply forward the result of another async method further, with the possible exception of using await in using .
public async Task MyMethodAsync() { await Task.Delay(1000); }
public Task MyMethodAsync() { return Task.Delay(1000); }
Why does this work? Because the await keyword can be applied to Task-like objects, and not to methods marked with the async keyword. In turn, the async keyword just tells the compiler that this method needs to be expanded into a state machine, and all the returned values ​​should be wrapped into a Task
(or into another Task-like object).
In other words, the result of the first version of the method is Task
, which will become Completed
as soon as the Task.Delay(1000)
, and the result of the second version of the method is Task
, returned by Task.Delay(1000)
itself Task.Delay(1000)
, which will become Completed
as soon as 1000 milliseconds pass. .
As you can see, both versions are equivalent, but at the same time, the first requires much more resources to create an asynchronous "body kit."
Alex Davis writes that costs directly to calling an asynchronous method can be ten times more than the cost of calling a synchronous method , so there is something to try for.
Source: https://habr.com/ru/post/435666/
All Articles