📜 ⬆️ ⬇️

Async / await in C #: pitfalls

I would like to discuss the pitfalls that are most often encountered when working with async / await features in C #, and also to write about how to get around them.

How async / await works


The async / await insides are well described by Alex Davies in his book , so I’ll only briefly describe them here. Consider the following sample code:

public async Task ReadFirstBytesAsync(string filePath1, string filePath2) { using (FileStream fs1 = new FileStream(filePath1, FileMode.Open)) using (FileStream fs2 = new FileStream(filePath2, FileMode.Open)) { await fs1.ReadAsync(new byte[1], 0, 1); // 1 await fs2.ReadAsync(new byte[1], 0, 1); // 2 } } 


This function reads one byte of the two files, the paths to which are passed through parameters. What happens in the lines “1” and “2”? Will they be executed in parallel? Not. This function will be “split” by the “await” keyword into three parts: the part preceding “1”, the part between “1” and “2”, and the part following “2”.
')
The function will start the new I / O bound thread in the “1” line, transfer the second part of itself (the part between “1” and “2”) as a callback and return control. After the I / O thread has completed its work, a callback will be called and the method will continue execution. The method will create another I / O stream in row “2”, pass it a third part of itself as a callback, and return control again. After the second I / O thread completes execution, the rest of the method will run.

The magic here is due to the compiler, which converts methods marked with the keyword "async" into a finite state machine, in the same way as it converts iterator methods .

When to use async / await?


There are two main scenarios in which the use of async / await is preferred.

First of all, this feature can be used in thick clients to provide users with a better user experience. When a user presses a button, starting a heavy computational operation, the best way out is to perform this operation asynchronously, without blocking the flow UI. Before .NET 4.5, this logic required much more effort. Now it can be programmed like this:

 private async void btnRead_Click(object sender, EventArgs e) { btnRead.Enabled = false; using (FileStream fs = new FileStream(“File path”, FileMode.Open)) using (StreamReader sr = new StreamReader(fs)) { Content = await sr.ReadToEndAsync(); } btnRead.Enabled = true; } 


Please note that the Enabled flag in both cases is set by the UI thread. This approach eliminates the need to write such ugly code:

 if (btnRead.InvokeRequired) { btnRead.Invoke((Action)(() => btnRead.Enabled = false)); } else { btnRead.Enabled = false; } 


In other words, the entire “light” code is executed by the calling thread, while the “heavy” parts are delegated to a separate thread (I / O or CPU-bound). This approach can significantly reduce the amount of effort required to synchronize access to UI elements, since they are only managed from the UI stream.

Secondly, async / await can be used in web applications for better thread utilization. The ASP.NET MVC team has made asynchronous controllers very easy to implement. You can simply write an action method like the example below and ASP.NET does the rest of the work:

 public class HomeController : Controller { public async Task<string> Index() { using (FileStream fs = new FileStream(“File path”, FileMode.Open)) using (StreamReader sr = new StreamReader(fs)) { return await sr.ReadToEndAsync(); // 1 } } } 


In this example, the workflow executing the method starts a new I / O stream on line “1” and returns to the thread pool. After the I / O thread finishes, the CLR selects a new thread from the pool and continues the method execution. Thus, CPU-bound threads from the thread pool are used much more economically.

Async / await in C #: pitfalls


If you are developing a third-party library, it is very important to always configure await so that the rest of the method is executed by an arbitrary thread from the pool. In other words, in the code of third-party libraries you should always add ConfigureAwait (false).

First of all, third-party libraries usually do not work with UI controls (unless of course this is not a UI library), so there is no need to bind a UI thread. You can increase performance slightly by allowing the CLR to execute your code with any thread from the pool. Secondly, using the default implementation (or explicitly stamping ConfigureAwait (true)), you leave a potential hole for deadlocks. Consider the following example:

 private async void button1_Click(object sender, EventArgs e) { int result = DoSomeWorkAsync().Result; // 1 } private async Task<int> DoSomeWorkAsync() { await Task.Delay(100).ConfigureAwait(true); //2 return 1; } 


Clicking on the button here leads to deadlock. The UI thread starts a new I / O thread on line “2” and goes into sleep mode on line “1”, waiting for the completion of work. After the I / O thread finishes execution, the rest of the DoSomeWorkAsync method is passed to the calling (UI) thread for execution. But he is at this time in sleep mode, waiting for the completion of the method. Dedlock.

ASP.NET behaves the same way. Despite the fact that in ASP.NET there is no dedicated UI thread, the code in the controller's action-ah can not be executed by more than one workflow at the same time.

Of course, we can use await instead of accessing the Result property to avoid deadlock:

 private async void button1_Click(object sender, EventArgs e) { int result = await DoSomeWorkAsync(); } private async Task<int> DoSomeWorkAsync() { await Task.Delay(100).ConfigureAwait(true); return 1; } 


But in .NET there is still at least one case in which you can’t get around the deadlock. You cannot use asynchronous methods inside ASP.NET MVC child action, because they are not supported. Thus, you will have to access the Result property directly and if the asynchronous method that is called by your controller is not configured correctly, you will get deadlock. For example, if you try to write the following code and SomeAction accesses the Result property of an asynchronous method that was not configured via ConfigureAwait (false), you will again get a deadlock:

 @Html.Action(“SomeAction“, “SomeController“) 


Users of your libraries usually do not have direct access to the code of these libraries, so always put ConfigureAwait (false) in advance in your asynchronous methods.

How not to use PLINQ and async / await


Consider an example:

 private async void button1_Click(object sender, EventArgs e) { btnRead.Enabled = false; string content = await ReadFileAsync(); btnRead.Enabled = true; } private Task<string> ReadFileAsync() { return Task.Run(() => // 1 { using (FileStream fs = new FileStream(“File path”, FileMode.Open)) using (StreamReader sr = new StreamReader(fs)) { return sr.ReadToEnd(); // 2 } }); } 


Is this code executed asynchronously? Yes. Is this code a valid example of writing asynchronous code? Not. The UI thread here starts a new CPU-bound thread on line “1” and returns control. This thread then starts a new I / O thread on line “2” and goes into sleep mode, waiting for execution.

What is going on here? Instead of creating a single I / O stream, we create both a CPU stream on the “1” line and an I / O stream on the “2” line. This is a waste of threads. To fix the situation, we need to use the asynchronous version of the Read method:

 private Task<string> ReadFileAsync() { using (FileStream fs = new FileStream(“File path”, FileMode.Open)) using (StreamReader sr = new StreamReader(fs)) { return sr.ReadToEndAsync(); } } 


One more example:

 public void SendRequests() { _urls.AsParallel().ForAll(url => { var httpClient = new HttpClient(); httpClient.PostAsync(url, new StringContent(“Some data”)); }); } 


It looks like we're sending requests in parallel, isn't it? Yes, it is, but here we have the same problem as in the previous example: instead of creating a single I / O stream, we create both I / O and CPU-bound stream for each request. You can fix the situation using the Task.WaitAll method:

 public void SendRequests() { IEnumerable<Task> tasks = _urls.Select(url => { var httpClient = new HttpClient(); return httpClient.PostAsync(url, new StringContent(“Some data”)); }); Task.WaitAll(tasks.ToArray()); } 


Is it always necessary to perform I / O operations without binding CPU-bound threads?


Depends on the situation. In some cases this is not possible, in some cases it introduces too much complexity to the code. For example, in NHibernate there are no possibilities for asynchronous loading of data from the database. EntityFramework, on the other hand, does have it, but using it does not always make sense.

Also, thick clients (for example, WPF or WinForms applications) usually do not have large loads, so for such an optimization for the most part is not necessary. But in any case, you need to know what is happening “under the hood” of this feature in order to be able to make a conscious decision in each specific case.

Link to original article: Async / await in C #: pitfalls

Source: https://habr.com/ru/post/257221/


All Articles