Tuesday, December 5, 2017

Waiting on multiple C# .Net awaits

Introduction

Async and Await makes developers life easy without the callback hell in asynchronous programming. But it is equally harmful, if it is in the hands of typewriting coders. Mainly those who don't know how things are working can use async and await in wrong way. This post examines one of such scenario and how to avoid it.

Lets consider the below example. There are 2 independent web service calls to be made and once result is available, do some operation using the results from both the async calls.

private static async Task<string> GetFirstTask(HttpClient client)
{
            Log(nameof(GetFirstTask));
            return await client.GetStringAsync("http://httpbin.org/drip?numbytes=3&duration=3&code=200");
}
private static async Task<string> GetSecondTask(HttpClient client)
{
            Log(nameof(GetSecondTask));
            return await client.GetStringAsync("http://httpbin.org/drip?numbytes=6&duration=6&code=200");
}
private void Process(string first, string second)
{
            Log($"{nameof(Process)} - Length of first is {first.Length} & second is {second.Length}");
}
private static void Log(string msg)
{
            Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}, Time {DateTime.UtcNow.ToLongTimeString()}, Message {msg}");
}

The first 2 methods returns 2 generic Task<string>. The URL is using httpbin.org which is a hosted service for testing purpose. The duration in the query string controls the delay. Meaning the response will be coming after that duration. Just to avoid Thread.Sleep(). The Process() just display it's parameters.

The normal way

Below is the code we can see more from new async await users.

async internal Task TestNormal_TheBadMethod()
{
    HttpClient client = new HttpClient();
    string firstrequest = await GetFirstTask(client);
    string secondrequest = await GetSecondTask(client);

    Process(firstrequest, secondrequest);
}

The output might be something like below.

Thread 1, Time 8:47:00 PM, Message GetFirstTask
Thread 9, Time 8:47:02 PM, Message GetSecondTask
Thread 7, Time 8:47:07 PM, Message Process - Length of first is 3 & second is 6

Problem

The line where GetFirstTask() is called will wait till the result is obtained. ie wait for 3 seconds to get response from web service. The second task will start only the first is completed. Clearly sequential.

await at method invocation

This is another way developers try.

async internal Task TestViaAwaitAtFunctionCall_StillBad()
{
    Log(nameof(TestViaAwaitAtFunctionCall_StillBad));
    HttpClient client = new HttpClient();
    Process(await GetFirstTask(client), await GetSecondTask(client));
}

Output will look as follows.

Thread 1, Time 8:49:22 PM, Message GetFirstTask
Thread 7, Time 8:49:25 PM, Message GetSecondTask
Thread 9, Time 8:49:30 PM, Message Process - Length of first is 3 & second is 6

Problem

In other languages await keyword at function invocation might make it parallel. But in C# its still sequential. It wait for first await and then process second.

Making it run parallel

So what is the solution? Both the Tasks should be created before we wait for their results. So those tasks will run in parallel. Once await is called, they just give the result if available or wait till the result is available. So the total time is the highest time, not sum of all wait times. Below code snippets does it.

private async Task TestViaTasks_Good()
{
            Log(nameof(TestViaTasks_Good));
            HttpClient client = new HttpClient();
            Task<string> firstrequest = GetFirstTask(client);
            Task<string> secondrequest = GetSecondTask(client);
            Process(await firstrequest, await secondrequest);
}

Output looks below.

Thread 1, Time 8:55:43 PM, Message GetFirstTask
Thread 1, Time 8:55:43 PM, Message GetSecondTask
Thread 8, Time 8:55:48 PM, Message Process - Length of first is 3 & second is 6

Here the Tasks are created before any waits are places on them. So they worked in parallel.

Will this work when second call dependent on first call's result

Not at all. Because the second call cannot start without the result from first call. So this has to be sequential.

More reading

https://stackoverflow.com/questions/33825436/when-do-multiple-awaits-make-sense
https://stackoverflow.com/questions/36976810/effects-of-using-multiple-awaits-in-the-same-method

No comments: