Any Difference Between "Await Task.Run(); Return;" and "Return Task.Run()"

Any difference between await Task.Run(); return; and return Task.Run()?

One major difference is in exception propagation. An exception, thrown inside an async Task method, gets stored in the returned Task object and remains dormant until the task gets observed via await task, task.Wait(), task.Result or task.GetAwaiter().GetResult(). It is propagated this way even if thrown from the synchronous part of the async method.

Consider the following code, where OneTestAsync and AnotherTestAsync behave quite differently:

static async Task OneTestAsync(int n)
{
await Task.Delay(n);
}

static Task AnotherTestAsync(int n)
{
return Task.Delay(n);
}

// call DoTestAsync with either OneTestAsync or AnotherTestAsync as whatTest
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
Task task = null;
try
{
// start the task
task = whatTest(n);

// do some other stuff,
// while the task is pending
Console.Write("Press enter to continue");
Console.ReadLine();
task.Wait();
}
catch (Exception ex)
{
Console.Write("Error: " + ex.Message);
}
}

If I call DoTestAsync(OneTestAsync, -2), it produces the following output:


Press enter to continue
Error: One or more errors occurred.await Task.Delay
Error: 2nd

Note, I had to press Enter to see it.

Now, if I call DoTestAsync(AnotherTestAsync, -2), the code workflow inside DoTestAsync is quite different, and so is the output. This time, I wasn't asked to press Enter:


Error: The value needs to be either -1 (signifying an infinite timeout), 0 or a positive integer.
Parameter name: millisecondsDelayError: 1st

In both cases Task.Delay(-2) throws at the beginning, while validating its parameters. This might be a made-up scenario, but in theory Task.Delay(1000) may throw too, e.g., when the underlying system timer API fails.

On a side note, the error propagation logic is yet different for async void methods (as opposed to async Task methods). An exception raised inside an async void method will be immediately re-thrown on the the current thread's synchronization context (via SynchronizationContext.Post), if the current thread has one (SynchronizationContext.Current != null). Otherwise, it will be re-thrown via ThreadPool.QueueUserWorkItem). The caller doesn't have a chance to handle this exception on the same stack frame.

I posted some more details about TPL exception handling behaviour here and here.


Q: Is it possible to mimic the exception propagation behavior of async methods for non-async Task-based methods, so that the latter doesn't throw on the same stack frame?

A: If really needed, then yes, there is a trick for that:

// async
async Task<int> MethodAsync(int arg)
{
if (arg < 0)
throw new ArgumentException("arg");
// ...
return 42 + arg;
}

// non-async
Task<int> MethodAsync(int arg)
{
var task = new Task<int>(() =>
{
if (arg < 0)
throw new ArgumentException("arg");
// ...
return 42 + arg;
});

task.RunSynchronously(TaskScheduler.Default);
return task;
}

Note however, under certain conditions (like when it's too deep on the stack), RunSynchronously could still execute asynchronously.


Another notable difference is that the async/await version is more prone to dead-locking on a non-default synchronization context. E.g., the following will dead-lock in a WinForms or WPF application:

static async Task TestAsync()
{
await Task.Delay(1000);
}

void Form_Load(object sender, EventArgs e)
{
TestAsync().Wait(); // dead-lock here
}

Change it to a non-async version and it won't dead-lock:

Task TestAsync() 
{
return Task.Delay(1000);
}

The nature of the dead-lock is well explained by Stephen Cleary in his blog.

async Task vs return Task.Run

Task.Run will queue its delegate to the thread pool. This causes two important things:

  1. Any code in DoStuff before the first asynchronous await will run on a thread pool thread, instead of on the calling thread.
  2. The code in DoStuff will run in a thread pool context, instead of using whatever context was current from the calling thread.

Most of the time, doing asynchronous work in Task.Run is a mistake. But it is sometimes useful, say if DoStuff does some heavy computational work before it acts asynchronously, then Task.Run could be used to move that work off of a UI thread.

Task.Run() difference for awaitable Task

Lambda expressions create private methods under the hood. So this:

Task.Run(() => DoSomething());

becomes something like this:

Task.Run(__lambda);
private Task __lambda() => DoSomething();

So, asking what the difference is between these two lines:

Task.Run(() => DoSomething());
Task.Run(async() => await DoSomething());

is the same as asking what the difference is between these two methods:

private Task __lambda() => DoSomething();
private async Task __lambda() => await DoSomething();

I have a blog post that goes into detail. The summary answer is that if all you are doing is just calling DoSomething, then you can elide (remove) the async and await keywords. If the lambda is doing anything else (e.g., modifying arguments) that was removed from your question for simplicity, then I'd recommend keeping the async and await keywords.

As a general guideline: any trivial code can elide async/await; any logic should use async and await.

What is the difference between Task.Run() and await Task.Run()?

What is the difference between Task.Run() and await Task.Run()?

The first starts a task and then does the work immediately after that task, before the task completes.

The second starts a task and then does different work until the task is completed, at which point it does the work after the task.

Let's make an analogy. Your first program is like doing this:

  • Hire someone to mow the lawn.
  • Tell your spouse the lawn is mowed.
  • Go watch Netflix.

Your second program is:

  • Hire someone to mow the lawn.
  • Watch Netflix while the lawn is being mowed.
  • When the lawn is done being mowed and the movie is over, tell spouse the lawn is mowed.

Clearly those are very different workflows. Both are asynchronous, but only the latter has an asynchronous wait in it. We asynchronously wait to tell the spouse that the lawn is mowed until it actually is mowed.

When correctly use Task.Run and when just async-await

Note the guidelines for performing work on a UI thread, collected on my blog:

  • Don't block the UI thread for more than 50ms at a time.
  • You can schedule ~100 continuations on the UI thread per second; 1000 is too much.

There are two techniques you should use:

1) Use ConfigureAwait(false) when you can.

E.g., await MyAsync().ConfigureAwait(false); instead of await MyAsync();.

ConfigureAwait(false) tells the await that you do not need to resume on the current context (in this case, "on the current context" means "on the UI thread"). However, for the rest of that async method (after the ConfigureAwait), you cannot do anything that assumes you're in the current context (e.g., update UI elements).

For more information, see my MSDN article Best Practices in Asynchronous Programming.

2) Use Task.Run to call CPU-bound methods.

You should use Task.Run, but not within any code you want to be reusable (i.e., library code). So you use Task.Run to call the method, not as part of the implementation of the method.

So purely CPU-bound work would look like this:

// Documentation: This method is CPU-bound.
void DoWork();

Which you would call using Task.Run:

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

Methods that are a mixture of CPU-bound and I/O-bound should have an Async signature with documentation pointing out their CPU-bound nature:

// Documentation: This method is CPU-bound.
Task DoWorkAsync();

Which you would also call using Task.Run (since it is partially CPU-bound):

await Task.Run(() => DoWorkAsync());

async Task then await Task vs Task then return task

It is almost the same (in terms of threads etc.). But for the second one (using await) a lot more overhead will be created by the compiler.

Methods declared as async and using await are converted into a state machine by the compiler. So when you hit the await, the control flow is returned to the calling method and execution of your async method is resumed after the await when the awaited Task has finished.

As there is no more code after your await, there is no need to use await anyway. Simply return the Task is enough.

what's the benefit of running async code with task.run()

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.



Related Topics



Leave a reply



Submit