Gdy poprawnie korzystasz z Task.Run i gdy tylko async-czekaj

317

Chciałbym zapytać Cię o Twoją opinię na temat prawidłowej architektury, kiedy używać Task.Run. Występuje opóźniony interfejs użytkownika w naszej aplikacji WPF .NET 4.5 (z ramą Caliburn Micro).

Zasadniczo robię (bardzo uproszczone fragmenty kodu):

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();
    }
}

Z artykułów / filmów, które przeczytałem / widziałem, wiem, że await asyncniekoniecznie działa na wątku w tle i aby rozpocząć pracę w tle, musisz go owinąć Task.Run(async () => ... ). Użycie async awaitnie blokuje interfejsu użytkownika, ale nadal działa w wątku interfejsu, więc powoduje, że jest on opóźniony.

Gdzie najlepiej umieścić Task.Run?

Powinienem tylko

  1. Zawiń zewnętrzne wywołanie, ponieważ jest to mniej wątkowe dla platformy .NET

  2. , czy powinienem zawijać tylko wewnętrznie działające metody związane z procesorem, Task.Runponieważ dzięki temu można go używać w innych miejscach? Nie jestem tutaj pewien, czy dobrym pomysłem jest rozpoczęcie pracy nad wątkami w tle głęboko w rdzeniu.

Ad (1) pierwsze rozwiązanie wyglądałoby tak:

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), drugie rozwiązanie wyglądałoby tak:

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
}
Lukas K.
źródło
BTW, wiersz w (1) await Task.Run(async () => await this.contentLoader.LoadContentAsync());powinien po prostu być await Task.Run( () => this.contentLoader.LoadContentAsync() );. AFAIK nic nie zyskujesz, dodając sekundę awaiti asyncwewnątrz Task.Run. A ponieważ nie przekazujesz parametrów, upraszcza to nieco bardziej await Task.Run( this.contentLoader.LoadContentAsync );.
ToolmakerSteve
jest tak naprawdę niewielka różnica, jeśli masz w środku drugą sekundę. Zobacz ten artykuł . Uznałem to za bardzo przydatne, właśnie w tym konkretnym punkcie nie zgadzam się i wolę zwracać bezpośrednio Zadanie zamiast czekać. (jak sugerujesz w komentarzu)
Lukas K

Odpowiedzi:

365

Zwróć uwagę na wytyczne dotyczące wykonywania pracy w wątku interfejsu użytkownika , zebrane na moim blogu:

  • Nie blokuj wątku interfejsu użytkownika przez więcej niż 50 ms jednocześnie.
  • Możesz zaplanować ~ 100 kontynuacji w wątku interfejsu użytkownika na sekundę; 1000 to za dużo.

Należy użyć dwóch technik:

1) Użyj, ConfigureAwait(false)kiedy możesz.

Np. await MyAsync().ConfigureAwait(false);Zamiast await MyAsync();.

ConfigureAwait(false)informuje await, że nie trzeba wznawiać w bieżącym kontekście (w tym przypadku „w bieżącym kontekście” oznacza „w wątku interfejsu użytkownika”). Jednak w pozostałej części tej asyncmetody (po ConfigureAwait) nie można zrobić niczego, co zakładałoby, że jesteś w bieżącym kontekście (np. Zaktualizować elementy interfejsu użytkownika).

Aby uzyskać więcej informacji, zobacz mój artykuł MSDN Najlepsze praktyki w programowaniu asynchronicznym .

2) Służy Task.Rundo wywoływania metod związanych z procesorem.

Powinieneś używać Task.Run, ale nie w żadnym kodzie, który ma być wielokrotnego użytku (tj. Kod biblioteki). Więc użyć Task.Runaby zadzwonić metody, a nie jako część realizacji tego sposobu.

Więc praca związana wyłącznie z procesorem wyglądałaby następująco:

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

Które nazwałbyś używając Task.Run:

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

Metody będące połączeniem procesora i operacji we / wy powinny mieć Asyncpodpis z dokumentacją wskazującą na ich charakter:

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

Który też wywołałbyś używając Task.Run(ponieważ jest częściowo związany z procesorem):

await Task.Run(() => DoWorkAsync());
Stephen Cleary
źródło
4
Dzięki za twój szybki resp! Znam link, który opublikowałeś i widziałem filmy, do których odwołuje się Twój blog. Właśnie dlatego opublikowałem to pytanie - w filmie jest napisane (tak samo jak w twojej odpowiedzi) nie powinieneś używać Task.Run w podstawowym kodzie. Ale moim problemem jest to, że muszę owijać taką metodę za każdym razem, gdy jej używam, aby nie spowalniać odpowiedzi (pamiętaj, że cały mój kod jest asynchroniczny i nie blokuje, ale bez Thread.Run jest po prostu opóźniony). Jestem również zdezorientowany, czy lepiej jest po prostu owinąć metody związane z procesorem (wiele wywołań Task.Run), czy całkowicie owinąć wszystko w jednym zadaniu Task.Run?
Lukas K
12
Wszystkie metody biblioteczne powinny być używane ConfigureAwait(false). Jeśli zrobisz to najpierw, może się okazać, że Task.Runjest to całkowicie niepotrzebne. Jeśli nie jeszcze trzeba Task.Run, to nie robi dużej różnicy do wykonywania w tym przypadku, czy to nazwać raz lub wiele razy, więc po prostu robić to, co jest najbardziej naturalny dla Twojego kodu.
Stephen Cleary
2
Nie rozumiem, jak pierwsza technika mu pomoże. Nawet jeśli użyjesz ConfigureAwait(false)metody powiązanej z procesorem, wciąż jest to wątek interfejsu użytkownika, który wykona metodę powiązaną z procesorem, i tylko wszystko po tym można zrobić w wątku TP. Czy coś źle zrozumiałem?
Dariusz
4
@ user4205580: Nie, Task.Runrozumie podpisy asynchroniczne, więc nie zostanie ukończony, dopóki nie DoWorkAsynczostanie ukończony. Dodatkowe async/ awaitjest niepotrzebne. Wyjaśniam więcej „dlaczego” w moim blogu na Task.Runetykiecie .
Stephen Cleary
3
@ user4205580: Nie. Zdecydowana większość „podstawowych” metod asynchronicznych nie używa go wewnętrznie. Normalny sposób do realizacji „rdzeń” metoda transmisji asynchronicznej jest użycie TaskCompletionSource<T>lub jednej z jego notacji skróconej, takie jak FromAsync. Mam wpis na blogu, który szczegółowo opisuje, dlaczego metody asynchroniczne nie wymagają wątków .
Stephen Cleary
12

Jednym z problemów z ContentLoaderem jest to, że wewnętrznie działa on sekwencyjnie. Lepszym wzorem jest ujednolicenie pracy, a następnie sychronizacja na końcu, więc otrzymujemy

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);
    }
}

Oczywiście nie działa to, jeśli którekolwiek z zadań wymaga danych z innych wcześniejszych zadań, ale powinno zapewnić lepszą ogólną przepustowość w większości scenariuszy.

Paul Hatcher
źródło