Когда правильно использовать Task.Run, а когда просто async-await

318

Я хотел бы спросить вас о вашем мнении о правильной архитектуре, когда использовать Task.Run. Я испытываю медленный пользовательский интерфейс в нашем приложении 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это не обязательно работает в фоновом потоке, и чтобы начать работу в фоновом режиме, нужно обернуть его с помощью await Task.Run(async () => ... ). Использование async awaitне блокирует пользовательский интерфейс, но, тем не менее, он работает в потоке пользовательского интерфейса, что делает его медленным.

Где лучше всего разместить Task.Run?

Должен ли я просто

  1. Оберните внешний вызов, потому что это менее трудоемкая работа для .NET

  2. , или я должен обернуть только связанные с процессором методы, которые выполняются внутри, Task.Runпоскольку это делает его многоразовым для других мест? Я не уверен, что начинать работу с фоновыми потоками в ядре - хорошая идея.

Объявление (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.

Объявление (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) await Task.Run(async () => await this.contentLoader.LoadContentAsync());должна быть просто await Task.Run( () => this.contentLoader.LoadContentAsync() );. AFAIK вы ничего не получите, добавив секунду awaitи asyncвнутри Task.Run. А так как вы не передаете параметры, это немного упрощает await Task.Run( this.contentLoader.LoadContentAsync );.
ToolmakerSteve
на самом деле есть небольшая разница, если у вас есть второе ожидание внутри. Смотрите эту статью . Я нашел это очень полезным, просто с этим конкретным пунктом я не согласен и предпочитаю возвращать задачу напрямую, а не ждать. (как вы предлагаете в своем комментарии)
Лукас К

Ответы:

365

Обратите внимание на рекомендации по выполнению работы в пользовательском интерфейсе , собранные в моем блоге:

  • Не блокируйте поток пользовательского интерфейса более чем на 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для вызова метода, а не как часть реализации метода.

Так что чисто работа с CPU будет выглядеть так:

// Documentation: This method is CPU-bound.
void DoWork();

Который вы бы назвали, используя Task.Run:

await Task.Run(() => DoWork());

Методы, которые являются смесью привязки к процессору и вводу-выводу, должны иметь Asyncподпись с документацией, указывающей на их привязку к процессору:

// Documentation: This method is CPU-bound.
Task DoWorkAsync();

Который вы бы также назвали используя Task.Run(так как он частично связан с процессором):

await Task.Run(() => DoWorkAsync());
Стивен Клири
источник
4
Спасибо за ваш быстрый ответ! Я знаю ссылку, которую вы разместили, и видел видео, на которые есть ссылки в вашем блоге. На самом деле, именно поэтому я разместил этот вопрос - в видео сказано (так же, как в вашем ответе), вы не должны использовать Task.Run в основном коде. Но моя проблема в том, что мне нужно оборачивать такой метод каждый раз, когда я его использую, чтобы не замедлять отклики (обратите внимание, что весь мой код асинхронный и не блокирует, но без Thread.Run он просто лагает). Меня также смущает, лучше ли просто обернуть связанные с процессором методы (много вызовов Task.Run) или полностью обернуть все в один Task.Run?
Лукас К
12
Все ваши методы библиотеки должны быть использованы ConfigureAwait(false). Если вы сделаете это сначала, то вы можете обнаружить, что Task.Runэто совершенно не нужно. Если вам все еще нужно Task.Run, это не имеет большого значения для времени выполнения в этом случае, если вы вызываете его один или несколько раз, так что просто делайте то, что наиболее естественно для вашего кода.
Стивен Клири
2
Я не понимаю, как первая техника поможет ему. Даже если вы используете ConfigureAwait(false)метод с привязкой к процессору, это все-таки поток пользовательского интерфейса будет выполнять метод с привязкой к процессору, и только все последующее может быть выполнено в потоке TP. Или я что-то не так понял?
Дарий
4
@ user4205580: Нет, Task.Runпонимает асинхронные подписи, поэтому он не будет завершен, пока не DoWorkAsyncбудет завершен. Дополнительный async/ awaitне нужен. Я объясняю больше "почему" в моей серии блогов на Task.Runэтикет .
Стивен Клири
3
@ user4205580: Нет. Подавляющее большинство «базовых» асинхронных методов не используют его внутренне. Нормальный способ реализации «основной» метод асинхронной заключается в использовании TaskCompletionSource<T>или один из его сокращенной записи обозначений , таких как FromAsync. У меня есть запись в блоге, в которой более подробно объясняется, почему асинхронные методы не требуют потоков .
Стивен Клири
12

Одна проблема с вашим 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);
    }
}

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

Пол Хэтчер
источник