Monday, June 22, 2015

Using async/await to run in parallel - a small nuance

I'm not going to go too far in to detailing how async/await work, you can find the documentation here. I am going to point out an easy to miss nuance that's obvious once you're aware of it's presence but not until you are.

Among many useful things async/await do they let you easily perform several actions in parallel, but you must remember to use them correctly.

Take this code for example.

static async Task DoWork()
{
    Task FirstTask = FirstWork();
    SecondWork();
    await FirstTask;

    Console.WriteLine("All tasks completed");
}

static async Task FirstWork()
{
    Console.WriteLine("First Work start");
    LengthySynchronousOperation();

    Console.WriteLine("First Work middle");
    
    await LengthyAsynchronousOperation();
    Console.WriteLine("First Work Done");
}

static void SecondWork()
{
    Console.WriteLine("Second Work");
}

Might look great at first glance but there's a problem. Looking at it here in idealized form the problem might be obvious - there's a Synchronous operation in there.
So what happens?
When we run it we get this.

First Work start
First Work middle
Second Work
First Work Done

All tasks completed

If you pay attention this isn't surprising - the execution of the calling method resumes only once the first await is hit - after the LongSynchronousOperation is executed. This isn't very parallel.
"But what's a synchronous operation doing there at all?" You might say. Of course you're better off making everything inherently asynchronous. But that might not be possible - perhaps you are using an external library that doesn't offer an asynchronous option for a method you need, perhaps you need to perform some operation that is either impossible or impractical to do asynchronously, what ever the reason is, it might happen.
Fortunately the solution is simple, you need to perform the synchronous function asynchronously, by changing the FirstWork method as follows:

static async Task FirstWork()
{
    Console.WriteLine("First Work start");

    await Task.Run((Action) LengthySynchronousOperation);

    Console.WriteLine("First Work middle");
    
    await LengthyAsynchronousOperation();
    Console.WriteLine("First Work Done");
}


Now everything works as you would expect, the two methods execute in parallel:

First Work start
Second Work
First Work middle
First Work Done
All tasks completed

Note:
If for some reason you can't or it's impractical to wrap everything neatly into asynchronous operations remember as soon as the first await is hit execution returns to the calling method, so you could await anything, even like this:

static async Task FirstWork()
{
    await Task.Delay(1);
    Console.WriteLine("First Work start");

    LengthySynchronousOperation();

    Console.WriteLine("First Work middle");
    
    await LengthyAsynchronousOperation();
    Console.WriteLine("First Work Done");
}

No comments:

Post a Comment