Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Refactoring Async/Await out for parallel processing

Still going through a learning phase with C# and ran into a question I needed help with. Considering the following code:

private async Task<String> PrintTask()
{
    await Task.Delay(3000);
    return "Hello";
}

private async void SayHelloTwice()
{
    string firstHello = await PrintTask();
    string secondHello = await PrintTask();

    Console.WriteLine(firstHello);
    Console.WriteLine(secondHello);
}

Right now SayHelloTwice() will take 6 seconds to complete. However, I want the retrieval tasks to be ran in parallel so that it only takes 3 seconds to complete. How would I refactor my code to achieve that? Thanks!

like image 722
Ben Avatar asked Dec 24 '22 07:12

Ben


2 Answers

The correct way to do this (without risk of deadlocks) is to use Task.WhenAll

private async void SayHelloTwice()
{
    string[] results = await Task.WhenAll(
        PrintTask(),
        PrintTask());

    Console.WriteLine(results[0]);
    Console.WriteLine(results[1]);
}

Quite often library writers will endeavor to make the "callback" code in a Task run on the original thread that called it. This is often because objects can only be accessed from a single thread.

When using the blocking calls such as Task.Result and Task.WaitAll the thread is suspended (cannot do additional work) until the Task completes.

However as previously mentioned, often times the Task will be waiting for the calling thread to free up, so it can be used to complete the task.

So, in the first case, the outter "Task" holds the Thread, and is waiting for completion.

The second case, the inner "Task" is waiting for the Thread to complete.

Ergo neither will ever run to completion.

like image 95
Aron Avatar answered Dec 29 '22 05:12

Aron


In general what you want is to start both tasks and wait afterwards. A straightforward way is:

private async void SayHelloTwice()
{
    // Start the tasks, don't wait
    Task<string> firstHello = PrintTask();
    Task<string> secondHello = PrintTask();

    // Wait for results
    string firstResult = await firstHello;
    string secondResult = await secondHello;

    // Print results
    Console.WriteLine(firstResult);
    Console.WriteLine(secondResult);
}

What happens here is the call to PrintTask() will start execution of the method and once the execution reaches the first await that does an actual async operation, the running task will be returned and assigned to firstHello. The same goes for secondHello. Then you wait for both to complete and print the result.

This implementation is just an example to simplify how things work. In real-world code you should probably use the Task.WhenAll to wait for all running tasks

like image 36
Itsik Avatar answered Dec 29 '22 07:12

Itsik