Як використовувати await у циклі


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

Відповіді:


123

Те, як ви використовуєте 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
Я не знаю про інших, але паралельний для / foreach здається більш прямим для паралельних петель.
Brettski

8
Важливо зазначити, що коли ви бачите Ending Processсповіщення, це не те, коли завдання фактично закінчується. Усі ці сповіщення викидаються послідовно відразу після завершення останнього із завдань. До того часу, коли ви побачите "Завершення процесу 1", процес 1, можливо, вже закінчився вже давно. Окрім вибору слів там, +1.
Asad Saeeduddin,

@Brettski Я, можливо, помиляюся, але паралельний цикл затримує будь-який результат асинхронізації. Повертаючи Task <T>, ви негайно повертаєте об'єкт Task, в якому ви можете керувати роботою, яка відбувається всередині, наприклад, скасувати її або побачити винятки. Тепер з Async / Await ви можете працювати з об'єктом Task більш дружньо - тобто вам не потрібно робити Task.Result.
The Muffin Man

@Tim S, а що, якщо я хочу повернути значення за допомогою функції асинхронізації за допомогою методу Tasks.WhenAll?
Міхір,

Чи буде це погана практика для реалізації Semaphoreін , DoWorkAsyncщоб обмежити максимум виконання завдань?
C4d

39

Ваш код чекає awaitзакінчення кожної операції (використання ) перед початком наступної ітерації.
Тому ви не отримуєте жодного паралелізму.

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

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

так що ви не можете використовувати будь-які асинхронні методи в будь-яких циклах?
Сатіш,

4
@Satish: Можна. Тим НЕ менше, awaitробить прямо протилежне тому, що ви хочете - це чекає для Taskзакінчити.
Слакс

Я хотів прийняти вашу відповідь, але Tims S має кращу відповідь.
Сатіш

Або якщо вам не потрібно знати, коли завдання закінчено, ви можете просто викликати методи, не чекаючи їх
disklosr

Щоб підтвердити, що робить цей синтаксис - він запускає Завдання, що викликається DoWorkAsyncдля кожного елемента list(передаючи кожен елемент DoWorkAsync, який, я вважаю, має один параметр)?
jbyrd

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);
    }
}
// ...
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.