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

86

Я пытаюсь создать асинхронное консольное приложение, которое выполняет некоторую работу с коллекцией. У меня есть одна версия, которая использует параллельный цикл для другой версии, которая использует async / await. Я ожидал, что версия 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;
    }
}
Satish
источник

Ответы:

124

То, как вы используете awaitключевое слово, сообщает C #, что вы хотите ждать каждый раз при прохождении цикла, который не является параллельным. Вы можете переписать свой метод таким образом, чтобы делать то, что вы хотите, сохранив список Tasks и затем 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);
}
Тим С.
источник
3
Я не знаю о других, но параллельный for / foreach кажется более простым для параллельных циклов.
Brettski
8
Важно отметить, что когда вы видите Ending Processуведомление, это не означает, что задача действительно заканчивается. Все эти уведомления выгружаются последовательно сразу после завершения последней задачи. К тому времени, когда вы увидите «Ending Process 1», процесс 1, возможно, уже давно закончился. Кроме выбора слов там +1.
Асад Саидуддин
@Brettski Я могу ошибаться, но параллельный цикл улавливает любой асинхронный результат. Возвращая Task <T>, вы немедленно получаете обратно объект Task, в котором вы можете управлять работой, которая происходит внутри, например отменять ее или видеть исключения. Теперь с помощью Async / Await вы можете работать с объектом Task более дружелюбно - то есть вам не нужно выполнять Task.Result.
The Muffin Man
@Tim S, что, если я хочу вернуть значение с помощью асинхронной функции, используя метод Tasks.WhenAll?
Mihir
Будет ли плохой практикой реализовать Semaphorein DoWorkAsyncдля ограничения максимального числа выполняемых задач?
C4d
39

Ваш код ожидает awaitзавершения каждой операции (использования ) перед началом следующей итерации.
Следовательно, никакого параллелизма не будет.

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

return Task.WhenAll(list.Select(DoWorkAsync));
SLaks
источник
так что вы не можете использовать асинхронные методы в любых циклах?
Сатиш
4
@Satish: Можно. Тем не менее, awaitделает прямо противоположное тому, что вы хотите - это ждет для Taskзакончить.
SLaks
Я хотел принять ваш ответ, но у Tims S есть ответ получше.
Satish
Или, если вам не нужно знать, когда задача завершилась, вы можете просто вызвать методы, не дожидаясь их
disklosr
Чтобы подтвердить, что делает этот синтаксис - он запускает задачу, вызываемую DoWorkAsyncдля каждого элемента list(передавая каждый элемент DoWorkAsync, который, как я полагаю, имеет один параметр)?
jbyrd 04
12
public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5);
    Task.WhenAll(series.Select(i => DoWorkAsync(i)));
    return true;
}
Владимир
источник
4

В C # 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);
}

Вы также могли бы избавиться от task. внутреннегоforeach :

// ...
foreach (var (IsDone, Index) in await Task.WhenAll(tasks))
{
    if (IsDone)
    {
        Console.WriteLine("Ending Process {0}", Index);
    }
}
// ...
Мехди Дехгани
источник