Как использовать ожидание в цикле

Я пытаюсь создать асинхронное консольное приложение, которое выполняет некоторую работу над коллекцией. У меня есть одна версия, которая использует параллельную для цикла другую версию, использующую async/wait. Я ожидал, что версия async/await будет работать аналогично параллельной версии, но будет выполняться синхронно. Что я делаю неправильно?

class Program
{
    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;
    }
}

Ответ 1

То, как вы используете ключевое слово await, указывает С#, что вы хотите ждать каждый раз, когда вы проходите через цикл, который не является параллельным. Вы можете переписать свой метод таким образом, чтобы сделать то, что вы хотите, сохранив список Task, а затем await со всеми их с помощью 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);
}

Ответ 2

Ваш код ждет завершения каждой операции (используя await) до начала следующей итерации.
Поэтому вы не получаете никаких parallelism.

Если вы хотите параллельно выполнять существующую асинхронную операцию, вам не нужно await; вам просто нужно получить коллекцию Task и вызвать Task.WhenAll(), чтобы вернуть задачу, ожидающую их всех:

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

Ответ 3

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

Ответ 4

В С# 7.0 вы можете использовать семантические имена для каждого из членов кортежа, вот ответ Тима С. с использованием нового синтаксиса:

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

Ответ 5

Иногда все, что нам нужно, это готовое решение copy + paste, таким образом, это все решение Tim.S. :

namespace AsyncSimple
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;

    class Startup
    {
        static void Main()
        {
            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();
            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);
        }
        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;
        }
    }
}

И результат в консоли:

enter image description here