1

It's hard for me the fathom the benefit of offloading asynchronous work to Task.Run(). there have been many questions about async work being offloaded to a Task before, but I have never seen, what is for me the biggest mystery, being addressed.

consider two options:

  • option1:
var t1= Task.Run( async() =>
        {
            await CPUBoundWorkAsync();
        });
  • option2:
var t2= Task.Run( () =>
        {
             CPUBoundWork();
        });

With the 2nd option, the work is being offloaded to a separate thread, thus asynchronicity is "achieved", and the main thread can get back to whatever it's needed for. With the 1st option, the Async method is used and the delegate is expressed as Async. Both options should yield indistinguishable result in terms of not blocking the main thread, and having the CPU bound work finish at the same time. Is there a benefit from using the Async method in Task.Run ? it seems to me this is essentially letting the work that is already being ran asynchronously (on a different non blocking thread), to also run itself asynchronously. Can the the thread pool or whatever manages those task threads read into this? and then perhaps give that awaiting thread some other work to do ? if this is the case I haven't seen it mentioned in any expert blog post, or relevant page on the internet.

Ben Guri
  • 306
  • 3
  • 12
  • Are `f1` and `f2` unchanged between the two variations? Meaning that in the first variation you await `f1` *before* calling `f2`, whereas in the second variation you call `f1`, disregard the `Task` object being returned, *do not* (a)wait for it to complete and then immediately call `f2`, possibly while `f1` is still executing? – Lasse V. Karlsen May 16 '22 at 19:22
  • @LasseV.Karlsen you are right, edited the question. – Ben Guri May 16 '22 at 19:34
  • 1
    Is your question related to this? [await Task.Run vs await](https://stackoverflow.com/questions/38739403/await-task-run-vs-await) – Theodor Zoulias May 16 '22 at 19:37
  • @TheodorZoulias that question is addressing something different and far more basic – Ben Guri May 16 '22 at 19:43
  • Does this answer your question? [await Task.Run vs await](https://stackoverflow.com/questions/38739403/await-task-run-vs-await) – MKR May 16 '22 at 21:12
  • @MKR it answers a different question – Ben Guri May 16 '22 at 21:17
  • To be honest I don't understand the question. You are presenting two options, that are only relevant in the minority of cases where both a synchronous and an asynchronous API is available for the same functionality (`Read`/`AsyncRead` in your example). But you are not asking which one is better, because I assume that you already know that. So you are not comparing really the option1 with the option2, but the option1 with the option3, without saying what the option3 is, and implying that it's not the natural `var t3 = AsyncRead();`. Would you like to clarify? – Theodor Zoulias May 16 '22 at 21:39
  • 1
    @TheodorZoulias Stephen Cleary understood and answered it perfectly on his answer below. He pointed out that the method return to the thread pool upon 'awating' thus the thread returns and can execute other work, which is what 'benefits' from doing so in the Task.Run() context – Ben Guri May 17 '22 at 16:09
  • @Ben Stephen Cleary might be better at mind-reading than the rest of us, and understood what you really wanted to know from what little info you gave in the question. So if your intention was to solicitate an answer by Stephen, goal achieved! :-) – Theodor Zoulias May 17 '22 at 16:34
  • I can't say if the question is inside or outside my knowledge comfort zone, because I don't understand the question. When someone presents two options, normally they want a comparison between these two options. This is not what you want, and that's why I am confused. – Theodor Zoulias May 17 '22 at 16:54
  • @TheodorZoulias I think it's pretty clear and I revised it again to make it even clearer, evidently the answer provided by Stephen managed to answer it accurately and shortly. the question touches on a detail that is important yet nuanced, and that the simple answer for it is missing from many sources I checked, including expert blog posts, and online learning classes. I feel that this question might serve few more developers that might stumble on an async in the Task.Run context and be riddled the same way. – Ben Guri May 17 '22 at 17:06
  • It is still not clear to me, because I don't know what you want to compare with what. If the question is *"why should I use X when there is no other option"*, then the obvious answer is *"you are forced to use X because there is no other option"*. Please consider presenting meaningful options, so that the question is meaningful. – Theodor Zoulias May 17 '22 at 17:13
  • Btw if you think that by changing the method names in the question from `AsyncRead`/`Read` to `CPUBoundWorkAsync`/`CPUBoundWork` the question is now more clear, at least for me it's not. The name `CPUBoundWorkAsync` specifically creates to me contradicting expectations about what this method might do internally. The `CPUBound`-part signifies that it does heavy use of at least one thread, and the `Async`-part that it uses [no thread](https://blog.stephencleary.com/2013/11/there-is-no-thread.html). I am curious what other people think about it. – Theodor Zoulias May 17 '22 at 17:13
  • @TheodorZoulias obviously I did more than just changing the names, but do as you like – Ben Guri May 17 '22 at 17:15

3 Answers3

2

With the 2nd option, the work is already being offloaded to a separate thread, then the asynchronicity is already gained in the scope of the program that invoked t2.

Task.Run doesn't make something asynchronous. It does permit you to run synchronous code on a thread pool thread (and thus blocking a thread pool thread instead of some other thread), but I wouldn't say it "makes it asynchronous". The code is still blocking a thread, synchronously.

Now although the work that is being done in the 1st option is declared and implemented as async, it's essentially letting code that is already being ran asynchronously, to also run itself asynchronously. What benefits from it ? Can the the scheduler or whatever manages those task threads read into this? and then perhaps give that awaiting thread some work on another task ?

Threads don't await. Methods await. When a thread is running a method and hits an await, that method is paused, the method returns to its caller, and the thread continues executing. In the case of a delegate passed to Task.Run, the await will cause the method to return -- to the thread pool, thus returning the thread to the thread pool. When the awaited task completes, a thread from the thread pool will be used to resume executing the method; this may be the same thread or a completely different thread.

More info on how exactly await works is on my blog.

if this is the case I haven't seen it mentioned in any expert blog post, or relevant page on the internet.

I have a series on Task.Run etiquette. Essentially, Task.Run is primarily used to offload synchronous or CPU-bound work off the UI thread in GUI applications. It is also occasionally useful in server-side scenarios if you want to start something processing quickly and loop back around and grab the next thing to process. There are a few other use cases but they are rare.

These reasons can all hold for asynchronous APIs. Sometimes APIs appear asynchronous but aren't actually. Or some methods are asynchronous but they do a nontrivial amount of synchronous work first, so a Task.Run is still desirable in some cases even with asynchronous code.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Hi, thank you. The detail about the thread returning to the thread pool answers the question. I had the feeling that should be the behavior, but it was not explicitly stated anywhere that I looked, perhaps it's too nuanced or obvious to most programmers (?) – Ben Guri May 17 '22 at 16:05
0

In your example, if f1() and f2() are asynchronous (in that they return Task) then the async keyword will allow you to await those inside your Task.Run().

Your question is similar to “why would you mark any method as async”… because you want to run asynchronous code inside. Or more specifically, you’d like the compiler to build the state machine to handle “waiting” for the asynchronous operation to complete.

Scott Perham
  • 2,410
  • 1
  • 10
  • 20
  • I edited the question, obviously I didn't mean that f1() and f2() return immediately – Ben Guri May 16 '22 at 19:44
  • I realised that just now… Its essentially the same thing though… if want to call async methods inside then use async, if not, don't :) It's not like there is a specific "benefit" to doing it, just depends on what the code inside the lambda is up to… the benefits are the same as defining any code as asynchronous and letting the compiler do the heavy lifting around the state of that task – Scott Perham May 16 '22 at 19:50
-1

I am not sure whether or not I get you question right. Focussing on "it's essentially letting code that is already being ran asynchronously, to also run itself asynchronously":

Although it seems pointless to run async code within Task.Run, there are situations where this can be nesessary. Asynchronous methods always run synchronously until the first "real" asynchronous call happens. A good example is the use of backgroundservices.

Clemens
  • 588
  • 6
  • 9