5

I've recently come across this code in a WinForm application and I can't figure if there is any reason to run async code inside a Task.Run that is awaited.

public async Task SaveStuff()
{
    await Task.Run(() => SaveStuffAsync().ConfigureAwait(false));
    await Task.Run(() => SendToExternalApiAsync().ConfigureAwait(false));
}

private async Task SaveStuffAsync()
{
    await DbContext.SaveChangesAsync().ConfigureAwait(false);
}

private async Task SendToExternalApiAsync()
{
    // some async code that is awaited with ConfigureAwait(false);
}

Wouldn't this code do the exact same thing without the Task.Run?

public async Task SaveStuff()
{
    await SaveStuffAsync().ConfigureAwait(false);
    await SendToExternalApiAsync().ConfigureAwait(false);
}
4
  • 2
    There is a always a possibility that a method such as SaveStuffAsync may block the calling thread despite the fact that it is declared as an async method. Calling it on a thread pool thread makes sure that your UI won't freeze regardless of the actual implementation of the (potentially) async method.
    – mm8
    Commented Aug 7, 2019 at 13:54
  • @peter-duniho So which question was a duplicate of this one exactly?
    – Kinetic
    Commented Aug 7, 2019 at 18:30
  • All of them discuss the differences between invoking a non-async method via Task.Run(), an async method via Task.Run(), and awaiting an async method directly. So...all of them. Commented Aug 7, 2019 at 18:31
  • @peter-duniho there's no non-async method involved at all in my question.
    – Kinetic
    Commented Aug 8, 2019 at 12:01

2 Answers 2

7

Wouldn't this code do the exact same thing without the Task.Run?

If the code inside the async method is actually asynchronous, it will not really make a difference. The Task would be executed on the threadpool, so it may take some more resources to execute. From the calling thread point of view, you would not notice the difference.

If, however, the code inside your async method is (not a)synchronous, you will notice a difference. Consider the following method:

private async Task DoWorkNotReallyAsync()
{
    for (int i = 0; i < aVeryLargeNumber; i++)
    {
        DoSynchronousComputation();
    }
}

The above method has an async signature, but will actually be run synchronously, thus blocking the calling thread while it executes. Wrapping the call in a Task.Run will schedule the execution to the threadpool. Therefore wrapping tasks in Task.Run might be useful if you want to be certain that a call to the async method will not block the current thread.

The methods called in your example do look truly asynchronous, so I wouldn't see a reason to wrap those tasks in Task.Run.

4
  • even if the method is actually asynchronous, it still may have long cpu computations which will block
    – johnny 5
    Commented Aug 7, 2019 at 14:00
  • @johnny5 I guess I'd call that not actually asynchronous in that case. Commented Aug 7, 2019 at 14:07
  • 1
    If you're making making calls over the network, or io operations, you're asynchronous, and if you do computations on that data I would still say you're actually asynchronous. The only time you aren't actually asynchronous, is the case you've provided, where only do computational work
    – johnny 5
    Commented Aug 7, 2019 at 14:24
  • As an example, by default EF Core's SaveChangesAsync (from the example) does change detection before it saves. This will block the caller. Commented Aug 7, 2019 at 15:23
4

The fact that a method returns a Taskdoesn't mean it yields back immediately. It might have have some time/CPU consuming setup before an I/O operation, for example.

For that reason, it is usual to see, on client UIs, everything outside of the UI being called inside Task.Run.

That being said, this is not such a case:

public async Task SaveStuff()
{
    await Task.Run(() => SaveStuffAsync().ConfigureAwait(false));
    await Task.Run(() => SendToExternalApiAsync().ConfigureAwait(false));
}

That causes one extra execution in the UI thread only to schedule work on the thread pool.

this would be more acceptable:

public async Task SaveStuff()
{
    await Task.Run(
        async () =>
        {
            await SaveStuffAsync();
            await SendToExternalApiAsync();
        });
}

There's no need to invoke ConfigureAwait(false) because it's guaranteed to not have a SynchronizationContext.

The difference between this and your last snippet is where and how that code is being invoked,

Not the answer you're looking for? Browse other questions tagged or ask your own question.