При правильном использовании 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);
    }
}

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