Использование async/ожидание нескольких задач

Я использую API-клиент, который полностью асинхронен, то есть каждая операция возвращает Task или Task<T>, например:

static async Task DoSomething(int siteId, int postId, IBlogClient client)
{
    await client.DeletePost(siteId, postId); // call API client
    Console.WriteLine("Deleted post {0}.", siteId);
}

Используя операторы async/wait С# 5, какой правильный/наиболее эффективный способ запускать несколько задач и ждать их завершения:

int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());

или

int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());

Поскольку клиент API использует HttpClient внутренне, я ожидаю, что он немедленно выдает 5 HTTP-запросов, записывая их на консоль, как только они завершатся.

Ответ 1

int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());

Несмотря на то, что вы выполняете операции параллельно с указанным выше кодом, этот код блокирует каждый поток, в котором выполняется каждая операция. Например, если сетевой вызов занимает 2 секунды, каждый поток зависает в течение 2 секунд без каких-либо действий, кроме ожидания.

int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());

С другой стороны, приведенный выше код с WaitAll также блокирует потоки, и ваши потоки не будут свободны обрабатывать любую другую работу до завершения операции.

Рекомендуемый подход

Я бы предпочел WhenAll, который будет выполнять ваши операции асинхронно в Parallel.

public async Task DoWork() {

    int[] ids = new[] { 1, 2, 3, 4, 5 };
    await Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}

Фактически, в приведенном выше случае вам даже не нужно await, вы можете просто прямо вернуться из метода, поскольку у вас нет никаких продолжений:

public Task DoWork() 
{
    int[] ids = new[] { 1, 2, 3, 4, 5 };
    return Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}

Чтобы поддержать это, вот подробное сообщение в блоге, проходящее через все альтернативы и их преимущества/недостатки: Как и где параллельный асинхронный ввод-вывод с веб-интерфейсом ASP.NET

Ответ 2

Мне было любопытно увидеть результаты методов, предоставленных в вопросе, а также принятый ответ, поэтому я поставил его на тест.

Здесь код:

class Program
{
    class Worker
    {
        public int Id { get; set; }
        public int SleepTimeout { get; set; }

        public async Task DoWork()
        {
            Console.WriteLine("Worker {0} started on thread {1} at {2}.",
                Id, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("hh:mm:ss.fff"));
            await Task.Run(() => Thread.Sleep(SleepTimeout));
            Console.WriteLine("Worker {0} stopped at {1}.",
                Id, DateTime.Now.ToString("hh:mm:ss.fff"));
        }
    }

    static void Main(string[] args)
    {
        var workers = new List<Worker>
        {
            new Worker { Id = 1, SleepTimeout = 3000 },
            new Worker { Id = 2, SleepTimeout = 3000 },
            new Worker { Id = 3, SleepTimeout = 3000 },
            new Worker { Id = 4, SleepTimeout = 3000 },
            new Worker { Id = 5, SleepTimeout = 3000 },
        };

        Console.WriteLine("Starting test: Parallel.ForEach");
        PerformTest_ParallelForEach(workers);
        Console.WriteLine("Test finished.\n");

        Console.WriteLine("Starting test: Task.WaitAll");
        PerformTest_TaskWaitAll(workers);
        Console.WriteLine("Test finished.\n");

        Console.WriteLine("Starting test: Task.WhenAll");
        var task = PerformTest_TaskWhenAll(workers);
        task.Wait();
        Console.WriteLine("Test finished.\n");

        Console.ReadKey();
    }

    static void PerformTest_ParallelForEach(List<Worker> workers)
    {
        Parallel.ForEach(workers, worker => worker.DoWork().Wait());
    }

    static void PerformTest_TaskWaitAll(List<Worker> workers)
    {
        Task.WaitAll(workers.Select(worker => worker.DoWork()).ToArray());
    }

    static Task PerformTest_TaskWhenAll(List<Worker> workers)
    {
        return Task.WhenAll(workers.Select(worker => worker.DoWork()));
    }
}

И получившийся результат:

Test Output

Ответ 3

Поскольку API, который вы вызываете, является асинхронным, версия Parallel.ForEach не имеет большого смысла. Вы не должны использовать .Wait в версии WaitAll, так как это приведет к потере parallelism Другой альтернативы, если вызывающий оператор async использует Task.WhenAll после выполнения Select и ToArray для генерации массива задач. Второй альтернативой является использование Rx 2.0