How to use await in a loop

C#.NetAsync Await

C# Problem Overview


I'm trying to create an asynchronous console app that does a some work on a collection. I have one version which uses parallel for loop another version that uses async/await. I expected the async/await version to work similar to parallel version but it executes synchronously. What am I doing wrong?

public class Program 
{
    public static void Main(string[] args) 
    {
        var worker = new Worker();
        worker.ParallelInit();
        var t = worker.Init();
        t.Wait();
        Console.ReadKey();
    }
}

public class Worker 
{
    public async Task<bool> Init() 
    {
        var series = Enumerable.Range(1, 5).ToList();
        foreach(var i in series) 
        {
          Console.WriteLine("Starting Process {0}", i);
          var result = await DoWorkAsync(i);
          
          if (result) 
          {
            Console.WriteLine("Ending Process {0}", i);
          }
        }

        return true;
    }

    public async Task<bool> DoWorkAsync(int i) 
    {
        Console.WriteLine("working..{0}", i);
        await Task.Delay(1000);
        
        return true;
    }

    public bool ParallelInit() 
    {
        var series = Enumerable.Range(1, 5).ToList();
        
        Parallel.ForEach(series, i => 
        {
            Console.WriteLine("Starting Process {0}", i);
            DoWorkAsync(i);
            Console.WriteLine("Ending Process {0}", i);
        });
        
        return true;
    }

}

C# Solutions


Solution 1 - C#

The way you're using the await keyword tells C# that you want to wait each time you pass through the loop, which isn't parallel. You can rewrite your method like this to do what you want, by storing a list of Tasks and then awaiting them all with Task.WhenAll.

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5).ToList();
    var tasks = new List<Task<Tuple<int, bool>>>();
    foreach (var i in series)
    {
        Console.WriteLine("Starting Process {0}", i);
        tasks.Add(DoWorkAsync(i));
    }
    foreach (var task in await Task.WhenAll(tasks))
    {
        if (task.Item2)
        {
            Console.WriteLine("Ending Process {0}", task.Item1);
        }
    }
    return true;
}

public async Task<Tuple<int, bool>> DoWorkAsync(int i)
{
    Console.WriteLine("working..{0}", i);
    await Task.Delay(1000);
    return Tuple.Create(i, true);
}

Solution 2 - C#

Your code waits for each operation (using await) to finish before starting the next iteration.
Therefore, you don't get any parallelism.

If you want to run an existing asynchronous operation in parallel, you don't need await; you just need to get a collection of Tasks and call Task.WhenAll() to return a task that waits for all of them:

return Task.WhenAll(list.Select(DoWorkAsync));

Solution 3 - C#

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5);
    Task.WhenAll(series.Select(i => DoWorkAsync(i)));
    return true;
}

Solution 4 - C#

In C# 7.0 you can use semantic names to each of the members of the tuple, here is Tim S.'s answer using the new syntax:

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5).ToList();
    var tasks = new List<Task<(int Index, bool IsDone)>>();

    foreach (var i in series)
    {
        Console.WriteLine("Starting Process {0}", i);
        tasks.Add(DoWorkAsync(i));
    }

    foreach (var task in await Task.WhenAll(tasks))
    {
        if (task.IsDone)
        {
            Console.WriteLine("Ending Process {0}", task.Index);
        }
    }

    return true;
}

public async Task<(int Index, bool IsDone)> DoWorkAsync(int i)
{
    Console.WriteLine("working..{0}", i);
    await Task.Delay(1000);
    return (i, true);
}

You could also get rid of task. inside foreach:

// ...
foreach (var (IsDone, Index) in await Task.WhenAll(tasks))
{
	if (IsDone)
	{
		Console.WriteLine("Ending Process {0}", Index);
	}
}
// ...

Solution 5 - C#

To add to the already good answers here, it's always helpful to me to remember that the async method returns a Task.

So in the example in this question, each iteration of the loop has await. This causes the Init() method to return control to its caller with a Task<bool> - not a bool.

Thinking of await as just a magic word that causes execution state to be saved, then skipped to the next available line until ready, encourages confusion: "why doesn't the for loop just skip the line with await and go to the next statement?"

If instead you think of await as something more like a yield statement, that brings a Task with it when it returns control to the caller, in my opinion flow starts to make more sense: "the for loop stops at await, and returns control and the Task to the caller. The for loop won't continue until that is done."

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSatishView Question on Stackoverflow
Solution 1 - C#Tim S.View Answer on Stackoverflow
Solution 2 - C#SLaksView Answer on Stackoverflow
Solution 3 - C#VladimirView Answer on Stackoverflow
Solution 4 - C#Mehdi DehghaniView Answer on Stackoverflow
Solution 5 - C#Aaron ThomasView Answer on Stackoverflow