Is Async/Await Suitable for Methods That Are Both Io and CPU Bound

Is async/await suitable for methods that are both IO and CPU bound?

There are two good answers already, but to add my 0.02...

If you're talking about consuming asynchronous operations, async/await works excellently for both I/O-bound and CPU-bound.

I think the MSDN docs do have a slight slant towards producing asynchronous operations, in which case you do want to use TaskCompletionSource (or similar) for I/O-bound and Task.Run (or similar) for CPU-bound. Once you've created the initial Task wrapper, it's best consumed by async and await.

For your particular example, it really comes down to how much time LoadHtmlDocument will take. If you remove the Task.Run, you will execute it within the same context that calls LoadPage (possibly on a UI thread). The Windows 8 guidelines specify that any operation taking more than 50ms should be made async... keeping in mind that 50ms on your developer machine may be longer on a client's machine...

So if you can guarantee that LoadHtmlDocument will run for less than 50ms, you can just execute it directly:

public async Task<HtmlDocument> LoadPage(Uri address)
{
using (var httpResponse = await new HttpClient().GetAsync(address)) //IO-bound
using (var responseContent = httpResponse.Content)
using (var contentStream = await responseContent.ReadAsStreamAsync()) //IO-bound
return LoadHtmlDocument(contentStream); //CPU-bound
}

However, I would recommend ConfigureAwait as @svick mentioned:

public async Task<HtmlDocument> LoadPage(Uri address)
{
using (var httpResponse = await new HttpClient().GetAsync(address)
.ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
using (var responseContent = httpResponse.Content)
using (var contentStream = await responseContent.ReadAsStreamAsync()
.ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
return LoadHtmlDocument(contentStream); //CPU-bound
}

With ConfigureAwait, if the HTTP request doesn't complete immediately (synchronously), then this will (in this case) cause LoadHtmlDocument to be executed on a thread pool thread without an explicit call to Task.Run.

If you're interested in async performance at this level, you should check out Stephen Toub's video and MSDN article on the subject. He has tons of useful information.

Why does CPU-Bound code lock GUI in async \ await unlike IO-Bound?

Asynchronous methods begin executing synchronously.

As noted in the article you linked, "You can use Task.Run to move CPU-bound work to a background thread":

private async Task<double> DoWork()
{
await Task.Run(() => TimeConsumingCPUBound());

await Task.Delay(10000);

return 0;
}

C# async/await for I/O-Bound vs CPU-Bound operation

To make your method async all you have to do is await an asynchronous operation within it, and make the method signature like this (Assuming the return value here...):

async Task<int> GetPageCountAsync()
{
//not shown is how to set up your connection and command
//nor disposing resources
//nor validating the scalar value
var pages = await command.ExecuteScalarAsync().ConfigureAwait(false);
return (int)pages;
}

If you have no async methods to call in the libraries you are using you can create your own awaitables but it should be extremely rare you need to go to such lengths.

Now that GetPageCountAsync is async you simply await it:

return await GetPageCountAsync();

It is also good practice for non-context aware code such as this to allow the code to resume on another context like:

return await GetPageCountAsync().ConfigureAwait(false);

If you have other work to do, not dependent on the result of the query, you can start it and await it later to perform work in parallel:

 Task pageCountTask = GetPageCountAsync();
//do something not dependent on page count....
return await pageCountTask;

Combining CPU-bound and IO-bound work in async method

for (int i = 0; i < numberOfRequests; i++)
{
tasks.Add(HandleRequest());
}

The returned task is created at the first await in the HandleRequest(). So you are executing all CPU bound code on one thread: the for loop thread. complete serialization, no parallelism at all.

When you wrap the CPU part in a task you are actually submitting the CPU part as Tasks, so they are executed in parallel.

Async and CPU-Bound Operations with ASP.NET

Mr Cleary is the one who opined about the fruitlessness of wrapping CPU-bound operations in async code.

Not exactly, there is a difference between wrapping CPU-bound async code in an ASP.NET app and doing that in a - for example - WPF desktop app. Let me use this statement of yours to build my answer upon.

You should categorize the async operations in your mind (in its simplest form) as such:

  • ASP.NET async methods, and among those:

    • CPU-bound operations,
    • Blocking operations, such as IO-operations
  • Async methods in a directly user-facing application, among those, again:

    • CPU-bound operations,
    • and blocking operations, such as IO-operations.

I assume that by reading Stephen Cleary's posts you've already got to understand that the way async operations work is that when you are doing a CPU-bound operation then that operation is passed on to a thread pool thread and upon completion, the program control returns to the thread it was started from (unless you do a .ConfigureAwait(false) call). Of course this only happens if there actually is an asynchronous operation to do, as I was wondering myself as well in this question.

When it comes to blocking operations on the other hand, things are a bit different. In this case, when the thread from which the code performed asynchronously gets blocked, then the runtime notices it, and "suspends" the work being done by that thread: saves all state so it can continue later on and then that thread is used to perform other operations. When the blocking operation is ready - for example, the answer to a network call has arrived - then (I don't exactly know how it is handled or noticed by the runtime, but I am trying to provide you with a high-level explanation, so it is not absolutely necessary) the runtime knows that the operation you initiated is ready to continue, the state is restored and your code can continue to run.

With these said, there is an important difference between the two:

  • In the CPU-bound case, even if you start an operation asynchronously, there is work to do, your code does not have to wait for anything.
  • In the IO-bound case or blocking case, however, there might be some time during which your code simply cannot do anything but wait and therefore it is useful that you can release that thread that has done the processing up until that point and do other work (perhaps process another request) meanwhile using it.

When it comes to a directly-user-facing application, for example, a WPF app, if you are performing a long-running CPU-operation on the main thread (GUI thread), then the GUI thread is obviously busy and therefore appears unresponsive towards the user because any interaction that is normally handled by the GUI thread just gets queued up in the message queue and doesn't get processed until the CPU-bound operation finishes.

In the case of an ASP.NET app, however, this is not an issue, because the application does not directly face the user, so he does not see that it is unresponsive. Why you don't gain anything by delegating the work to another thread is because that would still occupy a thread, that could otherwise do other work, because, well, whatever needs to be done must be done, it just cannot magically be done for you.

Think of it the following way: you are in a kitchen with a friend (you and your friend are one-one threads). You two are preparing food being ordered. You can tell your friend to dice up onions, and even though you free yourself from dicing onions, and can deal with the seasoning of the meat, your friend gets busy by dicing the onions and so he cannot do the seasoning of the meat meanwhile. If you hadn't delegated the work of dicing onions to him (which you already started) but let him do the seasoning, the same work would have been done, except that you would have saved a bit of time because you wouldn't have needed to swap the working context (the cutting boards and knives in this example). So simply put, you are just causing a bit of overhead by swapping contexts whereas the issue of unresponsiveness is invisible towards the user. (The customer neither sees nor cares which of you do which work as long as (s)he receives the result).

With that said, the categorization I've outlined at the top could be refined by replacing ASP.NET applications with "whatever application has no directly visible interface towards the user and therefore cannot appear unresponsive towards them".



Related Topics



Leave a reply



Submit