it-swarm.com.ru

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

Я хотел бы узнать ваше мнение о правильной архитектуре, когда использовать 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();
    }
}

Из статей/видео, которые я прочитал/увидел, я знаю, что awaitasync не обязательно работает в фоновом потоке, и чтобы начать работу в фоновом режиме, нужно обернуть его с помощью await Task.Run(async () => ... ). Использование asyncawait не блокирует пользовательский интерфейс, но, тем не менее, он работает в потоке пользовательского интерфейса, что делает его медленным.

Где лучше всего разместить 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
}
268
Lukas K

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

  • Не блокируйте поток пользовательского интерфейса более чем на 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());
317
Stephen Cleary

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

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

9
Paul Hatcher