-1
Task.Run(fn1sync());
Task.Run(fn2sync());

Above code starts each task in new thread, and runs them parallelly while returning control to main thread.

Instead of above, I can change the functions to use concept of async await and write:

var v1 = fn1Async()
var v2 = fn2Async()
...do some work..
Task.WaitAll(v1, v2)

Now .NET will auto handle whether to use single thread or multiple threads and also run them in parallel and immediately pass control to main thread.

When do I need to use either of these approaches?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
variable
  • 8,262
  • 9
  • 95
  • 215
  • 2
    [Task.Run Etiquette and Proper Usage](https://blog.stephencleary.com/2013/10/taskrun-etiquette-and-proper-usage.html), [Don't Use Task.Run for the Wrong Thing](https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html) and [Don't Use Task.Run in the Implementation](https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html) – Fildor Jul 23 '21 at 13:01
  • 2
    _"Instead of above, I can change the fns to use concept of async await and write:"_ nope, that's not the same. In your second example, the start of the method will run synchronously, that's not the case with the first example. Task.WaitAll will block the thread, you should use `await Task.WhenAll(....)` – Jeroen van Langen Jul 23 '21 at 13:10
  • 4
    Based on your other questions from today ( https://stackoverflow.com/q/68497903/982149 and https://stackoverflow.com/q/68498481/982149 ) which haven't been well-received unfortunately, maybe you should consider a book on that topic. Stephen C is always worth a read... – Fildor Jul 23 '21 at 13:17
  • 1
    The second example won't create any new threads and will block until all task end. There is also a chance of deadlock. Remember that `async`/`await` and threads are two completely separated things and neither is eye candy for the other. – Alejandro Jul 23 '21 at 13:21
  • Does any of these answers your question? 1. [Fire and forget, using Task.Run or just calling an async method without await](https://stackoverflow.com/questions/60778423/fire-and-forget-using-task-run-or-just-calling-an-async-method-without-await) 2. [Async/await with/without awaiting (fire and forget)](https://stackoverflow.com/questions/46053175/async-await-with-without-awaiting-fire-and-forget) 3. [Fire and forget async Task vs Task.Run](https://stackoverflow.com/questions/35857628/fire-and-forget-async-task-vs-task-run) – Theodor Zoulias Jul 23 '21 at 13:56
  • 1
    I recommend starting with my [async intro](https://blog.stephencleary.com/2012/02/async-and-await.html). – Stephen Cleary Jul 23 '21 at 16:04
  • @variable I noticed from the other questions you posted that are linked here that you're interested in parallel API calls. Are you by chance coming from the Javascript world? If so you can think of `Task` as equivalent to `Promise`, if that helps. – Emperor Eto Jul 23 '21 at 16:37
  • @variable to answer the title's question, No. `async` is just syntactic sugar that allows the compiler to use `await` to await an already active asynchronous operation represented by a Task. In other languages you'd call `Task` a `Promise`. That may be async IO, a job running on a thread, or some other async event converted to a `Task` through a TaskCompletionSource. On the other hand, `Task.Run` actually schedules a job represented by a lambda to run in a threadpool thread. – Panagiotis Kanavos Jul 26 '21 at 06:47

1 Answers1

4

I'm not sure the title of your question totally matches the actual question you're asking in the body, so I'll go with the body:

There's at least one really big difference between your two examples. With this (Option 1):

Task.Run(fn1sync());
Task.Run(fn2sync());

you have no way of waiting for both tasks to finish. They will run in parallel (PROBABLY - but not guaranteed - in different threads). They will both begin immediately and finish when they finish. Any code after that will also execute immediately, without waiting for the tasks to finish.

With this (Option 2):

var v1 = fn1Async()
var v2 = fn2Async()
...do some work..
Task.WaitAll(v1, v2)

You're holding onto the tasks and then after "do some work", you're explicitly waiting for both to finish before continuing. Nothing after the last line will execute until both are finished.

The question becomes more interesting - and maybe this is what you were getting at? - if you re-write option 1 this way (let's call it Option 3):

var v1 = Task.Run(fn1sync());
var v2 = Task.Run(fn2sync());
...do some work...
Task.WaitAll(v1, v2);

Are Option 2 and Option 3 identical? The answer is still no.

When you call an async method/delegate/etc. without awaiting it, which is what option 2 does, whether the delegate actually finishes before the calling branch can continue depends on whether the delegate invokes any truly async code. For example you could write this method:

public Task fn1sync()
{
    Console.Write("Task complete!");
    return Task.CompletedTask;
}

The Console.Write line will always execute before "do some work". Why? Because if you think of the return object Task literally, then fn1sync by definition can't be returning a Task to your calling code before it writes to the console (which is a synchronous operation). Replace "Console.Write" with something really slow, or maybe even blocking, and you've lost the benefits of async.

When you use Task.Run, on the other hand, you guarantee that the control flow will return to your next line of code immediately, no matter how long the delegate takes to execute and regardless whether the delegate actually contains any async code. You thus ensure that your calling thread won't get slowed down by whatever is in the delegate. In fact it's a great idea to use Task.Run when you have to invoke synchronous code that you have any reason to believe is going to be slow - legacy I/O, databases, native code, etc.

The use cases for calling an async method without awaiting it or wrapping it in Task.Run (Option 2) are perhaps less obvious but they do exist. It's a "fire and forget" approach that's fine as long as you know the code you're calling is well behaved async code (i.e. it's not going to block your thread). For example if fn1Async in Option 2 looked like this:

public Task fn1sync()
{
    return Task.Run(()=>
    {
         Console.Write("Task complete!");
    });
}

Then Options 2 and 3 would be fully equivalent and equally valid.

Update: To answer your question, "Can you tell me why does await in main thread cause it to block waiting for result, whereas await inside a function results in control passing to main thread?"

I agree with some of the comments that you might want to find a more comprehensive primer on async/await/Task. But that said, it's all about branching and how the await keyword is interpreted when it's compiled.

When you write:

async Task MyMethodAsync()
{
    Console.Write("A");
    int result = await AnotherMethodAsync();
    Console.Write("B");
    return;
}

This basically gets converted to:

Task MyMethodAsync()
{
    Console.Write("A");
    Task<int> task1 = AnotherMethodAsync();  // *should* return immediately if well behaved
    Task task2 = task1.ContinueWith((Task<int> t) => 
    {
        // t is same as task1 (unsure if they're identical instances but definitely equivalent)
        int result = t.Value;
        Console.Write("B");     ​
        ​return;
   ​ }); // returns immediately
    return task2; 
}

Everything before the first awaited statement in a method executes synchronously - on the caller's thread. But everything after the first awaited statement becomes a callback that gets executed only after the first awaited statement - task1 - is finished.

Importantly, ContinueWith returns its own Task - task2 and THAT is what is is returned to the caller of MyMethodAsync, allowing them to await it or not as they choose. The line "Console.Write("B") is guaranteed not to get executed until task1 is complete. However, when it gets executed relative to the code that follows the call to MyMethodAsync depends entirely on whether that call is awaited.

Thus:

async Task Main()
{
     await MyMethodAsync();
     Console.Write("C");
}

becomes:

Task Main()
{
     return MyMethodAsync().ContinueWith(t => 
     {
          // Doesn't get executed until MyMethodAsync is done
          Console.Write("C");
     });
}

and the output is thus guaranteed to be: ABC

However:

void Main()
{
     Task t = MyMethodAsync(); // Returns after "A" is written

     // Gets executed as soon as MyMethodAsync
     // branches to `await`ed code, returning the `Task`
     // May or may not be before "B"
     Console.Write("C");
}

executes literally, with t getting returned in an incomplete state, after "A" but before "C". The output is thus guaranteed to start with "A" but whether "B" or "C" will come next will not be predictable without knowing what AnotherMethodAsync is doing.

Emperor Eto
  • 2,456
  • 2
  • 18
  • 32
  • Can you tell me why does await in main thread cause it to block waiting for result, whereas await inside a function results in control passing to main thread? – variable Jul 23 '21 at 13:50
  • 1
    @JonasH I agree with Peter Moore. [Here](https://stackoverflow.com/questions/38739403/await-task-run-vs-await/58306020#58306020) are my arguments in favor of using `Task.Run` in async event handlers of GUI applications, for wrapping hand-coded asynchronous methods. The argument is basically that without `Task.Run` you risk blocking the UI thread, because what is advertised as asynchronous may actually have a synchronous behavior under uncertain circumstances. – Theodor Zoulias Jul 23 '21 at 16:06
  • @variable answered your follow up question in the body. Apologies for the length. Agreed with the other comments too. Modern async APIs are always preferable if available! – Emperor Eto Jul 23 '21 at 16:33
  • 1
    It's `async Task Main`, not `async void Main`. You can't await an `async void` method – Panagiotis Kanavos Jul 26 '21 at 06:43
  • As I am learning more, your answer is making sense. Im still bit confused around syncronous code inside the async function. Quick question - often I hear that database operation is io bound so it is sufficient to use async await without Task.Run. Whereas in your answer you mention that for databases it is better to use async await along with Task.Run so I'm bit confused please can you help me understand. – variable Jul 29 '21 at 18:04
  • Oh sorry, that's not what I meant. If you are using modern database (or any) APIs that have `await`able methods, you should use those APIs and there should be no need to use `Task.Run`. You should use `Task.Run` when you have synchronous code - yours or someone else's - that might block your thread. I'm thinking of legacy third party packages that might predate widespread use of `Task` and `async`. – Emperor Eto Jul 29 '21 at 19:47
  • Hey @variable would you kindly accept my answer if you're satisfied? Thanks! – Emperor Eto Jul 30 '21 at 01:31
  • You have mentioned that `The Console.Write line will always execute before "do some work"`. Cab you tell me will that line also always execute below the next line (fn2async) is invoked? – variable Aug 20 '21 at 16:45
  • 1
    @variable if you call `fn1async` naked without wrapping it in `Task.Run`, then yes., the `Console.Write` line is guaranteed to execute before `fn2async`. If you DO wrap the `fn1async` call in a `Task.Run`, then the order is not predictable. – Emperor Eto Aug 20 '21 at 16:52
  • Can you guide me where I can read up on how thread is managed. For example: 1) when await is encountered and control goes to caller, does caller use same thread or does the thread get released into threadpool. And a new thread gets assigned to the caller. 2) when await is complete after couple of seconds then the callback is executed by which thread. – variable Aug 20 '21 at 17:03
  • And I think the answer to my above points depend on whether it is a windows app or a console app. Because the windows app has a synchronization context. – variable Aug 20 '21 at 17:21
  • @variable yes it depends on whether the thread is a Single-Threaded Apartment (STA) - which is what WinForms, WPF, UWP, and WinUI apps use. (Not to get too pedantic but you CAN make an STA in a Console app). As for sources for more info I think others posted some really good links in the comments to your question. It can get VERY complicated to try to figure out exactly which thread is going to respond to the callbacks. The whole point of async/await is to not have to worry about that. – Emperor Eto Aug 20 '21 at 21:44