Recently I read an article on Habré ( upd: from the comments I realized that I need to attach a quote, on which further the question)

As soon as the code reaches the Task.Run () method, another thread gets from the thread pool and the code that we passed to Task.Run () is executed in it. 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 the time on context switching when calling Task.Run () and returning to ExecuteOperation (). Things got a little worse.

One of the issues that is considered there: the Task.Run call is the Task.Run - Task.Run , and is needed only for the responsiveness of the GUI.

The question is about Task.Run(() => _anyWork()) , where _anyWork() contains a synchronous code. What is written in the article sounds logical enough, if you do this:

 await DoWork(); ... Task DoWork() => Task.Run(_work); 

Yes, in this case, an extra load is created on the thread pool. But, if you do this:

 var task1 = DoWork1(); var task2 = DoWork2(); var task3 = DoWork3(); await Task.WhenAll(task1, task2, task3); 

Task.Run immediately turns into a normal code, right?

The thread that will execute this code will create three other threads ( upd : made a reservation: initiates the addition of work to the queue that will be launched in the ThreadPool ), which will simultaneously perform their work in parallel. Further, when he meets await , he will return control (as a result, he will most likely return to the pool). Please correct if not so.

If this is so, then the question arises: where does this line lie, between a bad implementation and a normal one? In a non-library code, it is clear that if a method is called and waiting somewhere further, then you can do Task.Run . In the library, on the one hand, we can parallelize the work of our methods if the client waits for them after the call. On the other - we can in vain increase the load if the client will expect the result immediately upon a call. To know exactly how the client will call methods - we cannot, we can only give recommendations in the documentation.

Perhaps there are some official MS recommendations? On msdn, I found only a dry description of how the methods work.

  • one
    1) this Task.Run does not create a new thread, but takes a ready stream from the pool - tym32167
  • one
    2) Task.Run сразу превращается в нормальный код, ведь так? What does a normal code mean? Critical code normality? - tym32167
  • one
    3) создается лишняя нагрузка на пул потоков pool of threads exists in order to load it. What does overload mean? - tym32167
  • 2
    It turns out the author wrote an inefficient code, with the assumption that this code is already taken from the thread pool, and at the same time he used Task.Run and you draw some conclusions about the applicability of Task.Run only by this example? Let's and C # ban m thread pool, they, too, in the example, have the same role as Task.Run - tym32167
  • 2
    The answer is simple - if you have a synchronous operation, but you do not want to block the current thread, then Task.Run is your choice. If the current flow doesn’t bother you, do it in sync - tym32167

3 answers 3

One of the issues that is considered there: the Task.Run call is the anti-pattern, and is needed only for the responsiveness of the GUI.

Task.Run Method

Queues the specified job to run in the ThreadPool and returns the task or Task Descriptor for the job.

That is, Task.Run is a way to do some work in a thread pool with the ability to wait for a result asynchronously.

When it may need? Possible examples of use:

1) Starting IO / CPU load of the thread pool so as not to load the main UI thread ( example )

2) Run asynchronous code synchronously from a UI stream. This may be required when you have a large application and you gradually move to asynchronous calls instead of synchronous, but not everywhere as long as you can call your API asynchronously, you should avoid deadlocks in the UI thread, for example Task.Run(()=>CallSmthgAsync().GetAwaiter().GetResult()).GetAwaiter().GetResult(); - this is not very good code and it should be corrected, but it is not always possible to introduce asynchronous calls in one go, because I myself did and saw it sometimes.

When it is not necessary: ​​when you do not care about the fact that the current thread is blocked for some kind of synchronous work.

In the example of the problem in question, the thread is already running in the thread pool, so switching to another thread and doing something synchronous doesn’t make sense there. But if the same work was launched from the UI thread, then without Task.Run entire UI application would be a stake.

As for the library code. Build your API and its implementation based on the requirements and use cases. If you have a lot of I / O operations (work with files, with a network, with a database), then this in fact itself hints at the need for an asynchronous API. If you are confused by the choice between synchronous API and asynchronous, then implement both, let the client choose what he needs.

Hence the conclusion: the most important thing is not to break wood, always know what you are doing and why you are doing it. Switch context when you have a reason to do it. If each line of your code is justified and you have a reason why it is written this way, then you will have much less problems with choosing good code / bad code.

  • And if I don't want to hang the GUI, is it worth doing so - await Task.Run(async () => await PartialAsyncMethod()); ? Visually, it turns out to be suspicious, but formally everything seems to be correct. - Monk
  • @Monk how will it differ from simple await PartialAsyncMethod() ? From the point of view of friezes, UI stream - tym32167 February
  • In my case, all processing will immediately leave the UI stream, in your case it will leave only when the synchronous code ends. Or not? I ask why. - Monk
  • one
    @Monk yes, but, given that we are all resource-intensive operations and so we execute asynchronously, then there are simple and fast operations for synchronous execution. In my case, yes, they will be executed in the UI thread, in your first the context switch will occur, then they will be executed in the background thread. What is faster here is a big question; context switching is a slow opera. Do simple and fast UI operations load flow? Yes, yes, but I do not think that the user will notice this. - tym32167
  • one
    @Monk By the way, keep in mind that we are discussing your code here, as if it was running from a UI thread. Running it from the background thread, of course, has no meaning. - tym32167

The code of the * Async method is executed in the current thread before the first await . And the current thread will be captured. For example, before starting an asynchronous request, HttpClient synchronously requests dns, which can be a lengthy operation.

And you can decide that you are more worried - waiting for the current thread or overhead from Task.Run () (which are minimal in fact, except that they inflate the code)

    use Task :

    • if inside you need to use an asynchronous operation:
      • there are several inside
      • synchronous code and asynchronous operation
      • Here is an interesting article about using(IDisposable) and a potential bug.
    • if you do your own asynchronous operation (for example, you turn an event into a Task ) - often use a TaskCompletionSource

    the second looks like an I/O-bound operation - not sure whether to write as a separate item

    • perhaps there will be useful information here - dgzargo
    • The so-called "potential bug" by reference is just a bug that can be created by carelessness or if you do not understand at all what you are doing. That is, that example is not something surprising and demanding to pay attention to itself. It is hardly surprising if you create a Task with the help of an object that, after its creation, immediately releases resources and returns a Task, which will probably work with the fields of the object whose resources were released. - CasperSC
    • aha I forgot that it works like finaly - I forgot to rewrite the code - and the "out of nothing" bug comes out! - dgzargo