📜 ⬆️ ⬇️

Misunderstanding about async / await and multithreading in C #

Hi, Habr! The async / await theme in the .NET Framework and C # 5.0 is not new and is crawled: everyone has known for a long time that this is how it works, everyone is familiar with the humble fact that it is a very flowing abstraction and behavior depends on the SynchronizationContext. About it very much wrote on Habré, more often this question was mixed up in blogs of various respectable people of the .NET-community.

Nevertheless, I very often have to deal with the fact that not only beginners, but also experienced team leaders do not quite understand how to properly use this tool in development.

My opinion is that the root of all evil lies in the opinion that async / await and Task Asynchronous Pattern should be used to write multi-threaded logic.

Having read a lot of information from various resources about async / await, developers form a myth: waiting happens in a separate thread, or code runs in a separate thread, or something else happens with a separate thread. No no and one more time no. Initially, TAP was conceived not for this purpose - for multithreading there is a Task Parallel Library ( MSDN ). Confusion arises not least due to the fact that Task is used in both TAP and TPL.
')
However, the code should have a clear separation between multi-threaded operations (CPU-bound) and asynchronous operations.
In my environment (ASP.NET), many of them work with Javascript. In this wonderful language, there is a simple pattern for asynchronous operations - callbacks, callback functions. In explaining the differences between TAP and TPL, I like to give the following Javascript example using jQuery:

$.get('/api/blabla', function(data) { console.log("Got some data."); }); console.log("Hello world!") 

In most cases, when executing the correct ajax request in the console, we will see the following:

Hello world!
Got some data.

That, in general, was expected. This is a very vivid example of asynchronous programming. There is no multithreading here - Javascript is strictly single-threaded and this code does not use any bloat like WebWorkers.

New-fashioned javascript libraries like to handle new ES6 (or ES2015?) Features for such tasks - Promise API. For example, similar code using $ http from AngularJS would look like this:

 $http.get('/api/blabla').success(function(data) { console.log("Got some data."); }); console.log("Hello world!") 

Here the $ http.get (...) call returns a Promise, to which you can attach a callback by calling success (...). The code, of course, still remains single-threaded.

And now we will consider a code similar in purpose on C #:

  var client = new WebClient(); client .DownloadStringTaskAsync("/api/blabla") .ContinueWith(result => { Console.WriteLine("Got some data."); }); Console.WriteLine("Hello world!"); 

Here client.DownloadStringTaskAsync returns a Task, ContinueWith attaches a callback to it. That is, in essence, Task and Promise are entities with the same idea in .NET and Javascript, respectively.

The same code can be written using await:

 var client = new WebClient(); var task = client.DownloadStringTaskAsync("/api/blabla"); Console.WriteLine("Hello world!"); var result = await task; Console.WriteLine("Got some data"); 

That is, await is a simple syntactic sugar over ContinueWith, which, among other things, is able to conveniently handle exceptions.
(UPD: of course, you shouldn't forget about SynchronizationContext: ContinueWith calls a callback in the thread from the thread pool, and all the code after await is implicitly executed on the context; to avoid this, use .ConfigureAwait (false) for Task. Thanks for the note kekekeks )

Why is this code good and correct? Because DownloadStringTaskAsync returns a Task that encapsulates an I / O operation — that is, an I / O bound operation. And almost all I / O is asynchronous — that is, for its implementation, nowhere, starting from the topmost level of the DownloadStringTaskAsync method call and ending with the network card driver, absolutely no additional stream is needed that will “wait” or “process” this operation .

Suppose for a second that we do not have a convenient API that returns Task, and we cannot use await to perform this asynchronous operation. Oddly enough, the developers of the .NET Framework from earlier versions created the API in such a way that it was possible to work with asynchronous I / O, and in the same WebClient class the outdated method remained to implement the same DownloadString using the Event Asynchronous Pattern (EAP) : You can subscribe to the DownloadStringCompleted event and call the DownloadStringAsync method.

Nevertheless, I very often come across the fact that, even if some legacy code provides an EAP API, if necessary, wrap it in TAP with experienced programmers simply come to the forehead:

 private Task<string> DownloadStringWithWebClientAsync(WebClient client, string url) { return Task.Run(() => client.DownloadString(url)); } 

What is the problem? And the problem, in fact, is that Task.Run runs the lambda () => client.DownloadString (url) passed into it in the new CPU-bound stream from the thread pool. While, in this case, there is no need for a separate stream.

How to "do it right"? Use TaskCompletionSource. Continuing the analogy with the Promise API, TaskCompletionSource performs the same functions as Deferred. Thus, you can create a Task that will not create additional threads. This is very handy when you need to wrap Task into waiting for an event to fire, such a scenario is well described in the example on MSDN .

So what happens is that Task Asynchronous Pattern cannot be used for multithreading? Can. But, as it was repeatedly mentioned in the articles to which I referred at the beginning, it is necessary:

a) Clear separation of CPU-bound and I / O-bound operations hidden behind Task.
b) If necessary, perform some operation in parallel, in a separate thread, it is better to allow resolving this situation to the calling code. For example, determine that all methods that return Task are I / O-bound, and you can use Task.Run in parallel to call CPU-bound methods.

Thanks for attention.

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


All Articles