Satish
Satish

Reputation: 3100

How to use await in a loop

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;
    }

}

Upvotes: 115

Views: 135737

Answers (6)

Kailash Mali
Kailash Mali

Reputation: 87

We can use async method in foreach loop to run async API calls.

public static void Main(string[] args)
{

List<ZoneDetails> lst = GetRecords();

    foreach (var item in lst)
    {
        //For loop run asyn
        var result = GetAPIData(item.ZoneId, item.fitnessclassid).Result;
        if (result != null && result.EventHistoryId != null)
        {
            UpdateDB(result);
        }
    }
}

private static async Task<FODBrandChannelLicense> GetAPIData(int zoneId, int fitnessclassid)
{

    HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

    var response = HttpClient.GetAsync(new Uri(url)).Result;

    var content = response.Content.ReadAsStringAsync().Result;
    var result = JsonConvert.DeserializeObject<Model>(content);

    if (response.EnsureSuccessStatusCode().IsSuccessStatusCode)
    {
        Console.WriteLine($"API Call completed successfully");

    }

    return result;
}

Upvotes: 1

Aaron Thomas
Aaron Thomas

Reputation: 5281

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."

Upvotes: 0

Mehdi Dehghani
Mehdi Dehghani

Reputation: 11601

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);
    }
}
// ...

Upvotes: 10

Tim S.
Tim S.

Reputation: 56536

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);
}

Upvotes: 156

Vladimir
Vladimir

Reputation: 7475

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

Upvotes: 14

SLaks
SLaks

Reputation: 887215

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));

Upvotes: 44

Related Questions