Parallel.Invoke не ждет, когда методы async завершатся

У меня есть приложение, которое извлекает достаточное количество данных из разных источников. Локальная база данных, сетевая база данных и веб-запрос. Любой из них может занять несколько секунд. Итак, сначала я решил запустить их параллельно:

Parallel.Invoke(
   () => dataX = loadX(),
   () => dataY = loadY(),
   () => dataZ = loadZ()
);

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

Затем я решил добавить в приложение счетчик или индикатор занятости. Я не хочу блокировать поток пользовательского интерфейса, или прядильщик не вращается. Поэтому их нужно запускать в async режиме. Но если я запускаю все три в асинхронном режиме, то они в аффекте происходят "синхронно", просто не в том же потоке, что и пользовательский интерфейс. Я все еще хочу, чтобы они побежали параллельно.

spinner.IsBusy = true;

Parallel.Invoke(
     async () => dataX = await Task.Run(() => { return loadX(); }),
     async () => dataY = await Task.Run(() => { return loadY(); }),
     async () => dataZ = await Task.Run(() => { return loadZ(); })
);

spinner.isBusy = false;

Теперь Parallel.Invoke не ждет, пока методы закончатся, и счетчик мгновенно отключится. Хуже того, dataX/Y/Z являются нулевыми, а исключения происходят позже.

Какая здесь правильная дорога? Должен ли я использовать BackgroundWorker вместо этого? Я надеялся использовать возможности.Net 4.5.

Ответ 1

Похоже, вы действительно хотите что-то вроде:

spinner.IsBusy = true;
try
{
    Task t1 = Task.Run(() => dataX = loadX());
    Task t2 = Task.Run(() => dataY = loadY());
    Task t3 = Task.Run(() => dataZ = loadZ());

    await Task.WhenAll(t1, t2, t3);
}
finally
{
    spinner.IsBusy = false;
}

Таким образом, вы асинхронно ожидаете завершения всех задач ( Task.WhenAll возвращает задачу, которая завершается, когда все остальные задачи завершаются), без блокировки потока пользовательского интерфейса... тогда как Parallel.InvokeParallel.ForEach т.д.) Являются блокируя вызовы и не должны использоваться в потоке пользовательского интерфейса.

(Причина, по которой Parallel.Invoke не блокировала ваш асинхронный lambdas, заключается в том, что он просто ждал, пока каждое Action вернется... что было в основном, когда оно попало в начало ожидания. Обычно вам нужно назначить асинхронную лямбда для Func<Task> или аналогичного, так же, как вы обычно не хотите писать async void методы.)

Ответ 2

Как вы сказали в своем вопросе, два из ваших методов запрашивают базу данных (один через sql, другой - через azure), а третий вызывает запрос POST для веб-службы. Все три из этих методов выполняют работу с привязкой к I/O.

Что произошло, когда вы вызываете Parallel.Invoke, вы в основном запускаете три потока ThreadPool для блокировки и ожидания завершения операций ввода-вывода, что в значительной степени является пустой тратой ресурсов, и будет очень сильно масштабироваться, если вам когда-либо понадобится.

Вместо этого вы можете использовать async apis, который все три из них выставляют:

  1. SQL Server через Entity Framework 6 или ADO.NET
  2. Azure имеет async api's
  3. Веб-запрос через HttpClient.PostAsync

Давайте предположим следующие методы:

LoadXAsync();
LoadYAsync();
LoadZAsync();

Вы можете вызвать их так:

spinner.IsBusy = true;
try
{
    Task t1 = LoadXAsync();
    Task t2 = LoadYAsync();
    Task t3 = LoadZAsync();

    await Task.WhenAll(t1, t2, t3);
}
finally
{
    spinner.IsBusy = false;
}

Это будет иметь тот же желаемый результат. Это не заморозит ваш пользовательский интерфейс, и это позволит вам сэкономить ценные ресурсы.