20

I'm getting the hang of the async/await keywords in C#, and how they facilitate asynchronous programming - allowing the the thread to be used elsewhere whilst some I/O bound task like a db call is going on.

I have read numerous times that async/await is for I/O bound tasks, not CPU-bound tasks. CPU-bound tasks should be performed on a separate background thread. Mentioned several times in these videos. All ok.

However, when starting long-running CPU-bound work on a new thread using Task.Run, you then have to await it at some point. So aren't we using async/await here for a CPU-bound task too? See example below.

public async Task SomeMethodAsync()
{
    int result = await Task.Run(() =>
    {
        // Do lots of CPU bound calculations...

        return result;
    }

    // Then do something with the result.
}
Tophat Gordon
  • 699
  • 2
  • 9
  • 22
  • 3
    The point is that I/O operation are async in the core of OS and then when you call kernel it will do it async anyway but for CPU operations you need to leverage threads to instruct CPU to do it async... – Johnny Feb 22 '18 at 13:33
  • You don't have to Task.Run to execute on a new thread. If you await and `ConfigureAwait(false)` you will (most likely) run that code on a different threadpool thread. And, are you implying you don't await I/O operations? You should always await at some point unless you are really sure you only need fire-and-forget. – Crowcoder Feb 22 '18 at 13:38
  • 3
    _"I have read numerous times that async/await is for I/O bound tasks, not CPU-bound tasks."_ citation needed. I doubt it was written exactly like this and you might misinterpret the message. – Fildor Feb 22 '18 at 13:45
  • You have explanation here https://learn.microsoft.com/en-us/dotnet/standard/async-in-depth. I think confusion is that for CPU-bound tasks you need to execute it from another thread but you still use await... – Johnny Feb 22 '18 at 13:51
  • 1
    With desktop GUI apps, a CPU bound operation may block the UI in the event handler, so often best to make the handler async and wrap the underlying operation in a Task.Run and await. With ASP.NET, the request already has a dedicated thread. Awaiting Task.Run for a CPU bound operation just leaves you with two threads doing the same work. The next request will still pick another thread from the pool even if the existing request is processing, async or not. With I/O bound requests you always want to leverage an available async operation and no additional thread will be created. – tjmoore Sep 11 '20 at 12:42

2 Answers2

27

async/await is the contemporary and easiest way to do async programming whether or not the job is I/O-bound; whether or not the Task requires a thread.

For example, it is great for WinForms or WPF because the main thread can await a child task to calculate the number of times Miss Piggy had lunch in a year (a rather long and complex CPU-bound operation let's say) and when complete executes the next line immediately below. It makes your code so intuitive unlike classical async callbacks; WaitOnes or other mechanisms and the juggling acts that go with it.

MSDN:

You can avoid performance bottlenecks and enhance the overall responsiveness of your application by using asynchronous programming. However, traditional techniques for writing asynchronous applications can be complicated, making them difficult to write, debug, and maintain.

Visual Studio 2012 introduces a simplified approach, async programming, that leverages asynchronous support in the .NET Framework 4.5 and the Windows Runtime. The compiler does the difficult work that the developer used to do, and your application retains a logical structure that resembles synchronous code. More...

OP:

I have read numerous times that async/await is for I/O bound tasks

Incorrect. async/await is shorthand for async programming where the compiler does more of the work. It is not just for I/O-bound tasks. CPU-bound tasks generally use a thread pool thread.

CPU-bound tasks should be performed on a separate background thread.

Yes...but that doesn't mean you can't await the Task. CPU-bound tasks unlike I/O-bound tasks require a thread to operate and by definition a worker thread and so will grab one from the available thread pool.

However, when starting long-running CPU-bound work on a new thread using Task.Run, you then have to await it at some point

You don't have to await a Task, such tasks would be known as fire-and-forget. The await doesn't actually start it either, tasks are "hot". However by not awaiting, you run the risk of the task not completing when the application exits. e.g. a console app firing off a Task and not awaiting it then exiting.

So aren't we using async/await here for a CPU-bound task too?

That's right, you can use it for any Task whether it is I/O-bound or not.

More

Community
  • 1
  • 1
  • 1
    your UI will not be freed up if you await CPU Bound Task - Downvote – Mark Wardell Jun 20 '23 at 19:03
  • @MarkWardell Incorrect. By default the task capture context send the task to the thread pool. It doesn't matter if the work is IO bound or not. If you're working with a Winforms / WPF application, then a different context is used and thus tasks are run on the main/ui/calling thread. Thats why `Task` has `.ConfigureAwait(bool continueOnCapturedContext)` to override/define how the await execution should proceed. – Kieran Devlin Aug 11 '23 at 17:14
4

If you await for something you have a promise that you will get results from that awaited operation. Normally you expect that awaited operation is asynchronous\background operation which will be executed in the future. In the meanwhile you can continue with your work which can be done independently from the result of that operation.

async/await does exactly that it is compiler help to archive that. await will return the control to the caller and process awaited operation asynchronously. That asynchronism could be provided in different ways.

I/O operations will be processed asynchronously once they are in kernel space, so you don't need to create additional thread for I/O operations.

After the System API call, the request is now in kernel space, making its way to the networking subsystem of the OS (such as /net in the Linux Kernel). Here the OS will handle the networking request asynchronously. Details may be different depending on the OS used (the device driver call may be scheduled as a signal sent back to the runtime, or a device driver call may be made and then a signal sent back), but eventually the runtime will be informed that the networking request is in progress. At this time, the work for the device driver will either be scheduled, in-progress, or already finished (the request is already out "over the wire") - but because this is all happening asynchronously, the device driver is able to immediately handle something else!

On the other hand for CPU operation you have to instruct somehow CPU to do the stuff asynchronously and typically this is done from different thread(the reason you have Task.Run).

CPU-bound async code is a bit different than I/O-bound async code. Because the work is done on the CPU, there's no way to get around dedicating a thread to the computation. The use of async and await provides you with a clean way to interact with a background thread and keep the caller of the async method responsive. Note that this does not provide any protection for shared data. If you are using shared data, you will still need to apply an appropriate synchronization strategy.

The point is that in both cases you have to await in order to wait for a promise to be fulfilled.

Johnny
  • 8,939
  • 2
  • 28
  • 33