Коли правильно використовувати Task.Run і коли просто async-wait


317

Я хотів би поцікавитись вашою думкою щодо правильної архітектури, коли користуватися 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це не обов'язково працює на фоновій нитці, і щоб розпочати роботу на задньому плані, потрібно обернути її в очікуванні 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

насправді є невелика різниця, якщо всередині вас чекають другі. Дивіться цю статтю . Я вважаю це дуже корисним, просто з цим конкретним моментом я не згоден і вважаю за краще повернутися безпосередньо Завданням, а не очікувати. (як ви підказуєте у своєму коментарі)
Лукаш K

Відповіді:


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для виклику методу, а не як частини реалізації методу.

Отже, суто пов'язана з процесором робота виглядатиме так:

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

Ви б зателефонували за допомогою Task.Run:

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

Методи, що є сумішшю CPU, пов'язаної з I / O, повинні мати 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);
    }
}

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.