1

I have a bunch of work to do (either CPU-bound or IO-bound with no async interface to use) within an asynchronous method that returns a Task. I'm wondering if it's OK to just do all the CPU-bound work within the asynchronous method like this:

async Task DoSomeStuff()
{
    await SomethingAsync();
    …
    DoCpuBoundWork();
    …
    await SomethingElseAsync();
}

or should I use Task.Run like this?

async Task DoSomeStuff()
{
    await SomethingAsync();
    …
    await Task.Run(() => DoCpuBoundWork());
    …
    await SomethingElseAsync();
}

I know tasks aren't necessarily executed on another thread, so I'm wondering if the scheduler might make assumptions about tasks being non-blocking that'll make doing CPU-bound work outside of Task.Run slow the application down. For example, if the scheduler decided to schedule the CPU-bound work to the app's UI thread, there could be a slow-down.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Arshia001
  • 1,854
  • 14
  • 19

2 Answers2

1

As noted in this page (section Deeper Dive into Task and Task<T> for a CPU-Bound Operation), tasks run on the thread from which they're called, and CPU-bound work should indeed be wrapped in a Task.Run so it'll run on another (background) thread. So yes, it's OK and normal to use Task.Run within an async method for CPU-bound or otherwise blocking work. Quoted from the page:

public async Task<int> CalculateResult(InputData data)
{
    // This queues up the work on the threadpool.
    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));

    // Note that at this point, you can do some other work concurrently,
    // as CalculateResult() is still executing!

    // Execution of CalculateResult is yielded here!
    var result = await expensiveResultTask;

    return result;
}

CalculateResult() executes on the thread it was called on. When it calls Task.Run, it queues the expensive CPU-bound operation, DoExpensiveCalculation(), on the thread pool and receives a Task handle. DoExpensiveCalculation() is eventually run concurrently on the next available thread, likely on another CPU core. It's possible to do concurrent work while DoExpensiveCalculation() is busy on another thread, because the thread which called CalculateResult() is still executing.

Once await is encountered, the execution of CalculateResult() is yielded to its caller, allowing other work to be done with the current thread while DoExpensiveCalculation() is churning out a result. Once it has finished, the result is queued up to run on the main thread. Eventually, the main thread will return to executing CalculateResult(), at which point it will have the result of DoExpensiveCalculation().

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Arshia001
  • 1,854
  • 14
  • 19
0

The answer is: it depends.

You pointed out the scheduler, that's exactly the problem. If you're running on the threadpool (default scheduler), then running synchronous CPU-bound work is perfectly fine. If you're running on the UI thread, this may lead to a poor user experience.

That said, while this is a problem, this is not your problem. Your contract, by implemented DoSomeStuff, is that you'll run some work on the current scheduler. It's up to the caller to figure out whether that could be an issue, and in that case add a Task.Run to offset the call to another scheduler.

In a nutshell, don't use await Task.Run(() => DoCpuBoundWork());. Let the caller, who knows the context of execution, decide for you.

Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94
  • It's pretty clear what the caller should do. I'm offering an awaitable Async method, they should await my method. The caller isn't going to do `await Task.Run(async () => await DoSomeStuff());` – Arshia001 Sep 08 '18 at 13:13
  • 1
    @Arshia001 Why not? If the caller has reasons to believe the method is going to run some expensive computations and don't want that to happen on the main thread, that's the purpose of `Task.Run`. And `await Task.Run(DoSomeStuff)` is enough, no need to await in the lambda – Kevin Gosse Sep 08 '18 at 15:19
  • Because that's what you'd normally do with async methods, you await them and don't think about it any more. – Arshia001 Sep 09 '18 at 06:38
  • 1
    @Arshia001 even built-in async methods can have a partially or fully synchronous implementations. For example the `File.ReadAllText` [was mostly blocking](https://stackoverflow.com/questions/63217657/why-file-readalllinesasync-blocks-the-ui-thread), until it was improved in .NET 6. If you are calling an asynchronous API on the UI thread of a GUI application directly, without `Task.Run`, you are risking the responsiveness of your application. Putting `Task.Run` everywhere can give you peace of mind, at a very low cost. The cost of `Task.Run` is minuscule, unless you are calling it in a loop. – Theodor Zoulias Feb 12 '23 at 19:35