При правильном использовании Task.Run и при простом асинхронном ожидании
Я хотел бы спросить вас о вашем мнении о правильной архитектуре при использовании Task.Run
. Я испытываю laggy UI в нашем WPF.NET 4.5
(с каркасом Caliburn Micro).
В основном я делаю (очень упрощенные фрагменты кода):
public class PageViewModel : IHandle<SomeMessage>
{
...
public async void Handle(SomeMessage message)
{
ShowLoadingAnimation();
// Makes UI very laggy, but still not dead
await this.contentLoader.LoadContentAsync();
HideLoadingAnimation();
}
}
public class ContentLoader
{
public async Task LoadContentAsync()
{
await DoCpuBoundWorkAsync();
await DoIoBoundWorkAsync();
await DoCpuBoundWorkAsync();
// I am not really sure what all I can consider as CPU bound as slowing down the UI
await DoSomeOtherWorkAsync();
}
}
Из статей/видео, которые я читал/видел, я знаю, что await
async
не обязательно работает в фоновом потоке и для начала работы в фоновом режиме вам нужно обернуть его с ожиданием Task.Run(async () => ... )
. Использование async
await
не блокирует пользовательский интерфейс, но все же он работает в потоке пользовательского интерфейса, поэтому он делает его медленным.
Где лучше всего разместить Task.Run?
Должен ли я просто
-
Оберните внешний вызов, потому что это меньше потоковой работы для .NET.
-
или я должен обернуть только методы с привязкой к ЦП, выполняемые внутри с помощью Task.Run
, поскольку это делает его многоразовым для других мест? Я не уверен, что если начать работу над фоновыми потоками в глубине ядра, это хорошая идея.
Ad (1), первое решение будет таким:
public async void Handle(SomeMessage message)
{
ShowLoadingAnimation();
await Task.Run(async () => await this.contentLoader.LoadContentAsync());
HideLoadingAnimation();
}
// Other methods do not use Task.Run as everything regardless
// if I/O or CPU bound would now run in the background.
Ad (2), второе решение будет таким:
public async Task DoCpuBoundWorkAsync()
{
await Task.Run(() => {
// Do lot of work here
});
}
public async Task DoSomeOtherWorkAsync(
{
// I am not sure how to handle this methods -
// probably need to test one by one, if it is slowing down UI
}
Ответы
Ответ 1
Обратите внимание на рекомендации для выполнения работы над потоком пользовательского интерфейса, собранные в моем блоге:
- Не блокируйте поток пользовательского интерфейса более чем на 50 мс за раз.
- Вы можете запланировать ~ 100 продолжений в потоке пользовательского интерфейса в секунду; 1000 слишком много.
Существует два метода, которые вы должны использовать:
1) Используйте ConfigureAwait(false)
, когда сможете.
Например, await MyAsync().ConfigureAwait(false);
вместо await MyAsync();
.
ConfigureAwait(false)
сообщает await
, что вам не нужно возобновлять текущий контекст (в этом случае "в текущем контексте" означает "в потоке пользовательского интерфейса" ). Однако для остальной части этого метода async
(после ConfigureAwait
) вы не можете делать ничего, что предполагает, что вы находитесь в текущем контексте (например, обновляете элементы пользовательского интерфейса).
Для получения дополнительной информации см. статью MSDN Рекомендации по асинхронному программированию.
2) Используйте Task.Run
для вызова методов, связанных с ЦП.
Вы должны использовать Task.Run
, но не внутри какого-либо кода, который вы хотите использовать повторно (например, код библиотеки). Поэтому вы используете Task.Run
для вызова метода, а не как часть реализации метода.
Таким образом, чисто работа с процессором будет выглядеть так:
// Documentation: This method is CPU-bound.
void DoWork();
Что вы бы назвали с помощью Task.Run
:
await Task.Run(() => DoWork());
Методы, являющиеся смесью привязанных к ЦП и I/O-привязке, должны иметь подпись async
с документацией, указывающей свой характер, связанный с ЦП:
// Documentation: This method is CPU-bound.
Task DoWorkAsync();
Которое вы также вызывали бы с помощью Task.Run
(поскольку он частично связан с ЦП):
await Task.Run(() => DoWorkAsync());
Ответ 2
Это один из способов сделать это. Правильный Task
может быть достигнут без Task.Run
... на переднем конце кода, потому что это побеждает цель. Если Task
- async
, вы должны просто вызвать await TaskAsync()
... Здесь git репо, у которого есть отличный пример: AsyncAwaitTasksExample. Он имеет встроенный таймер в пользовательском интерфейсе и опции для различных Tasks
и способы их использования. Единственное, что работает, похоже на ниже.
Task<String> GetStringAsync() {
return Task.Run<String>(() => {
return "Hello World";
});
}
//Run the task with Async await
void async SomeFunction() {
var msg = await GetStringAsync();
}
Ответ 3
Одна проблема с вашим ContentLoader заключается в том, что внутри он работает последовательно. Лучше всего распараллелить работу, а затем синхронизировать в конце, поэтому мы получаем
public class PageViewModel : IHandle<SomeMessage>
{
...
public async void Handle(SomeMessage message)
{
ShowLoadingAnimation();
// makes UI very laggy, but still not dead
await this.contentLoader.LoadContentAsync();
HideLoadingAnimation();
}
}
public class ContentLoader
{
public async Task LoadContentAsync()
{
var tasks = new List<Task>();
tasks.Add(DoCpuBoundWorkAsync());
tasks.Add(DoIoBoundWorkAsync());
tasks.Add(DoCpuBoundWorkAsync());
tasks.Add(DoSomeOtherWorkAsync());
await Task.WhenAll(tasks).ConfigureAwait(false);
}
}
Очевидно, что это не работает, если какая-либо из задач требует данных из других более ранних задач, но должна обеспечить лучшую общую пропускную способность для большинства сценариев.