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 await
ed 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 await
ed.
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.