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
async
niekoniecznie działa na wątku w tle i aby rozpocząć pracę w tle, musisz go owinąć Task.Run(async () => ... )
. Użycie async
await
nie 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
Zawiń zewnętrzne wywołanie, ponieważ jest to mniej wątkowe dla platformy .NET
, czy powinienem zawijać tylko wewnętrznie działające metody związane z procesorem,
Task.Run
ponieważ 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
}
źródło
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ęawait
iasync
wewnątrzTask.Run
. A ponieważ nie przekazujesz parametrów, upraszcza to nieco bardziejawait Task.Run( this.contentLoader.LoadContentAsync );
.Odpowiedzi:
Zwróć uwagę na wytyczne dotyczące wykonywania pracy w wątku interfejsu użytkownika , zebrane na moim blogu:
Należy użyć dwóch technik:
1) Użyj,
ConfigureAwait(false)
kiedy możesz.Np.
await MyAsync().ConfigureAwait(false);
Zamiastawait MyAsync();
.ConfigureAwait(false)
informujeawait
, ż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 tejasync
metody (poConfigureAwait
) 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.Run
do 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.Run
aby zadzwonić metody, a nie jako część realizacji tego sposobu.Więc praca związana wyłącznie z procesorem wyglądałaby następująco:
Które nazwałbyś używając
Task.Run
:Metody będące połączeniem procesora i operacji we / wy powinny mieć
Async
podpis z dokumentacją wskazującą na ich charakter:Który też wywołałbyś używając
Task.Run
(ponieważ jest częściowo związany z procesorem):źródło
ConfigureAwait(false)
. Jeśli zrobisz to najpierw, może się okazać, żeTask.Run
jest to całkowicie niepotrzebne. Jeśli nie jeszcze trzebaTask.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.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?Task.Run
rozumie podpisy asynchroniczne, więc nie zostanie ukończony, dopóki nieDoWorkAsync
zostanie ukończony. Dodatkoweasync
/await
jest niepotrzebne. Wyjaśniam więcej „dlaczego” w moim blogu naTask.Run
etykiecie .TaskCompletionSource<T>
lub jednej z jego notacji skróconej, takie jakFromAsync
. Mam wpis na blogu, który szczegółowo opisuje, dlaczego metody asynchroniczne nie wymagają wątków .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
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.
źródło