4

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!

svick
  • 236,525
  • 50
  • 385
  • 514
Ben
  • 159
  • 8
  • 1
    Just a comment on the terminology: "parallel" implies multiple threads. What you want is asynchronous *concurrency*, not *parallelism*. – Stephen Cleary Apr 06 '16 at 14:42

3 Answers3

4

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.

Aron
  • 15,464
  • 3
  • 31
  • 64
  • If you're going to bring in the threat of 'deadlocks' might as well explain when and where this is an issue. – NPSF3000 Apr 06 '16 at 01:43
  • 2
    This answer is correct I just feel like the whole deadlock explanation is redundant as it has nothing to do with the actual question – Itsik Apr 06 '16 at 02:03
  • Retracting downvote due to improvements, but agree with Itsik. I think the focus on deadlocks is unwarranted and risks turning useful advice into an unhelpful and vague warning - especially since this appears to be a console app (where as far as I'm aware, blocking isn't usually an issue). – NPSF3000 Apr 06 '16 at 04:15
3

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

Itsik
  • 3,920
  • 1
  • 25
  • 37
3
  • You can use Task.WhenAll to await multiple tasks.
  • You should also prefer returning a Task rather that void (async/await - when to return a Task vs void?):

    private static async Task SayHelloTwice()
    {
        var hellos = await Task.WhenAll(PrintTask(), PrintTask());
        Console.WriteLine(hellos[0]);
        Console.WriteLine(hellos[1]);
    }
    
Community
  • 1
  • 1
Thomas
  • 24,234
  • 6
  • 81
  • 125