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.Invoke
(и Parallel.ForEach
т.д.) Являются блокируя вызовы и не должны использоваться в потоке пользовательского интерфейса.
(Причина, по которой Parallel.Invoke
не блокировала ваш асинхронный lambdas, заключается в том, что он просто ждал, пока каждое Action
вернется... что было в основном, когда оно попало в начало ожидания. Обычно вам нужно назначить асинхронную лямбда для Func<Task>
или аналогичного, так же, как вы обычно не хотите писать async void
методы.)
Ответ 2
Как вы сказали в своем вопросе, два из ваших методов запрашивают базу данных (один через sql, другой - через azure), а третий вызывает запрос POST для веб-службы. Все три из этих методов выполняют работу с привязкой к I/O.
Что произошло, когда вы вызываете Parallel.Invoke
, вы в основном запускаете три потока ThreadPool для блокировки и ожидания завершения операций ввода-вывода, что в значительной степени является пустой тратой ресурсов, и будет очень сильно масштабироваться, если вам когда-либо понадобится.
Вместо этого вы можете использовать async apis, который все три из них выставляют:
- SQL Server через Entity Framework 6 или ADO.NET
- Azure имеет async api's
- Веб-запрос через
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;
}
Это будет иметь тот же желаемый результат. Это не заморозит ваш пользовательский интерфейс, и это позволит вам сэкономить ценные ресурсы.