Async / await vs BackgroundWorker

162

W ciągu ostatnich kilku dni testowałem nowe funkcje .net 4.5 i c # 5.

Podoba mi się jego nowe funkcje async / await. Wcześniej korzystałem z BackgroundWorker do obsługi dłuższych procesów w tle z responsywnym interfejsem użytkownika.

Moje pytanie brzmi: kiedy mam te fajne nowe funkcje, kiedy powinienem używać async / await, a kiedy BackgroundWorker ? Jakie są typowe scenariusze dla obu?

Tomek
źródło
Obie są dobre, ale jeśli pracujesz ze starszym kodem, który nie został przeniesiony do nowszej wersji .net; BackgroundWorker działa na obu.
dcarl661

Odpowiedzi:

74

async / await jest przeznaczony do zastępowania konstrukcji, takich jak BackgroundWorker. Chociaż z pewnością możesz go użyć, jeśli chcesz, powinieneś móc używać async / await, wraz z kilkoma innymi narzędziami TPL, do obsługi wszystkiego, co tam jest.

Ponieważ oba działają, sprowadza się to do osobistych preferencji, kiedy używasz. Co jest dla ciebie szybsze ? Co jest dla ciebie łatwiejsze do zrozumienia?

Servy
źródło
17
Dzięki. Dla mnie async / await wydaje się dużo bardziej przejrzyste i „naturalne”. BakcgoundWorker sprawia, że ​​kod moim zdaniem jest „hałaśliwy”.
Tom
12
@Tom Cóż, właśnie dlatego Microsoft poświęcił dużo czasu i wysiłku na jego wdrożenie. Gdyby nie było lepiej, nie przejmowaliby się
Servy
5
Tak. Nowe elementy await sprawiają, że stary BackgroundWorker wydaje się być całkowicie gorszy i przestarzały. Różnica jest taka dramatyczna.
usr
16
Na swoim blogu mam całkiem niezłe podsumowanie porównujące różne podejścia do zadań w tle. Zauważ, że async/ awaitrównież umożliwia programowanie asynchroniczne bez wątków puli wątków.
Stephen Cleary,
8
Odrzuciłem tę odpowiedź, jest myląca. Async / await NIE zastępuje procesu roboczego w tle.
Quango
206

To prawdopodobnie TL; DR dla wielu, ale myślę, że porównywanie awaitz porównaniem BackgroundWorkerjest jak porównywanie jabłek i pomarańczy, a moje przemyślenia na ten temat są następujące:

BackgroundWorkerma na celu modelowanie pojedynczego zadania, które chcesz wykonać w tle, w wątku puli wątków. async/ awaitto składnia asynchronicznego oczekiwania na operacje asynchroniczne. Te operacje mogą, ale nie muszą, używać wątku puli wątków lub nawet używać dowolnego innego wątku . Więc to jabłka i pomarańcze.

Na przykład możesz wykonać następujące czynności await:

using (WebResponse response = await webReq.GetResponseAsync())
{
    using (Stream responseStream = response.GetResponseStream())
    {
        int bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length);
    }
}

Ale prawdopodobnie nigdy nie modelowałbyś tego w pracy w tle, prawdopodobnie zrobiłbyś coś takiego w .NET 4.0 (przed await):

webReq.BeginGetResponse(ar =>
{
    WebResponse response = webReq.EndGetResponse(ar);
    Stream responseStream = response.GetResponseStream();
    responseStream.BeginRead(buffer, 0, buffer.Length, ar2 =>
    {
        int bytesRead = responseStream.EndRead(ar2);
        responseStream.Dispose();
        ((IDisposable) response).Dispose();
    }, null);
}, null);

Zwróć uwagę na rozłączność rozdysponowania w porównaniu między dwiema składniami i tym, jak nie możesz użyć usingbez async/ await.

Ale nie zrobiłbyś czegoś takiego z BackgroundWorker. BackgroundWorkersłuży zwykle do modelowania pojedynczej długotrwałej operacji, na którą nie chcesz mieć wpływu na responsywność interfejsu użytkownika. Na przykład:

worker.DoWork += (sender, e) =>
                    {
                    int i = 0;
                    // simulate lengthy operation
                    Stopwatch sw = Stopwatch.StartNew();
                    while (sw.Elapsed.TotalSeconds < 1)
                        ++i;
                    };
worker.RunWorkerCompleted += (sender, eventArgs) =>
                                {
                                    // TODO: do something on the UI thread, like
                                    // update status or display "result"
                                };
worker.RunWorkerAsync();

Naprawdę nie ma tam nic, z czym możesz użyć async / await, BackgroundWorkertworzy wątek dla Ciebie.

Teraz możesz zamiast tego użyć TPL:

var synchronizationContext = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() =>
                      {
                        int i = 0;
                        // simulate lengthy operation
                        Stopwatch sw = Stopwatch.StartNew();
                        while (sw.Elapsed.TotalSeconds < 1)
                            ++i;
                      }).ContinueWith(t=>
                                      {
                                        // TODO: do something on the UI thread, like
                                        // update status or display "result"
                                      }, synchronizationContext);

W takim przypadku TaskSchedulertworzy wątek dla Ciebie (zakładając domyślny TaskScheduler) i może użyć awaitw następujący sposób:

await Task.Factory.StartNew(() =>
                  {
                    int i = 0;
                    // simulate lengthy operation
                    Stopwatch sw = Stopwatch.StartNew();
                    while (sw.Elapsed.TotalSeconds < 1)
                        ++i;
                  });
// TODO: do something on the UI thread, like
// update status or display "result"

Moim zdaniem główne porównanie dotyczy tego, czy zgłaszasz postępy, czy nie. Na przykład możesz mieć BackgroundWorker liketo:

BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.ProgressChanged += (sender, eventArgs) =>
                            {
                            // TODO: something with progress, like update progress bar

                            };
worker.DoWork += (sender, e) =>
                 {
                    int i = 0;
                    // simulate lengthy operation
                    Stopwatch sw = Stopwatch.StartNew();
                    while (sw.Elapsed.TotalSeconds < 1)
                    {
                        if ((sw.Elapsed.TotalMilliseconds%100) == 0)
                            ((BackgroundWorker)sender).ReportProgress((int) (1000 / sw.ElapsedMilliseconds));
                        ++i;
                    }
                 };
worker.RunWorkerCompleted += (sender, eventArgs) =>
                                {
                                    // do something on the UI thread, like
                                    // update status or display "result"
                                };
worker.RunWorkerAsync();

Ale nie poradziłbyś sobie z niektórymi z tego, ponieważ przeciągnąłbyś i upuść komponent roboczy w tle na powierzchnię projektu formularza - coś, czego nie możesz zrobić z async/ awaiti Task... tj. Wygrałeś ” Utwórz obiekt ręcznie, ustaw właściwości i ustaw programy obsługi zdarzeń. chcesz wypełnić tylko w organizmie z DoWork, RunWorkerCompletedi ProgressChangedobsługi zdarzeń.

Jeśli „przekonwertowałeś” to na async / await, zrobiłbyś coś takiego:

     IProgress<int> progress = new Progress<int>();

     progress.ProgressChanged += ( s, e ) =>
        {
           // TODO: do something with e.ProgressPercentage
           // like update progress bar
        };

     await Task.Factory.StartNew(() =>
                  {
                    int i = 0;
                    // simulate lengthy operation
                    Stopwatch sw = Stopwatch.StartNew();
                    while (sw.Elapsed.TotalSeconds < 1)
                    {
                        if ((sw.Elapsed.TotalMilliseconds%100) == 0)
                        {
                            progress.Report((int) (1000 / sw.ElapsedMilliseconds))
                        }
                        ++i;
                    }
                  });
// TODO: do something on the UI thread, like
// update status or display "result"

Bez możliwości przeciągania komponentu na powierzchnię Projektanta, tak naprawdę to czytelnik decyduje, co jest „lepsze”. Ale to jest dla mnie porównanie między awaita BackgroundWorker, a nie tym, czy można czekać na wbudowane metody, takie jak Stream.ReadAsync. np. jeśli używasz BackgroundWorkerzgodnie z przeznaczeniem, konwersja do użycia może być trudna await.

Inne przemyślenia: http://jeremybytes.blogspot.ca/2012/05/backgroundworker-component-im-not-dead.html

Peter Ritchie
źródło
2
Myślę, że jedną z wad async / await jest to, że możesz chcieć uruchomić wiele zadań asynchronicznych jednocześnie. await oznacza czekanie na zakończenie każdego zadania przed rozpoczęciem kolejnego. A jeśli pominiesz słowo kluczowe await, metoda będzie działać synchronicznie, co nie jest tym, czego chcesz. Nie sądzę, aby async / await rozwiązało problem, taki jak „rozpocznij te 5 zadań i oddzwoń do mnie, gdy każde zadanie zostanie wykonane w dowolnej kolejności”.
Trevor Elliott
4
@Moozhe. Nieprawda, możesz to zrobić var t1 = webReq.GetResponseAsync(); var t2 = webReq2.GetResponseAsync(); await t1; await t2;. Który czekałby na dwie równoległe operacje. Oczekiwanie jest znacznie lepsze w przypadku zadań asynchronicznych, ale sekwencyjnych, IMO ...
Peter Ritchie
2
@Moozhe tak, robienie tego w ten sposób zachowuje określoną sekwencję - jak wspomniałem. jest to główny punkt oczekiwania polega na uzyskaniu asynchroniczności w sekwencyjnym kodzie. Oczywiście await Task.WhenAny(t1, t2)możesz coś zrobić, gdy jedno z zadań zostanie ukończone jako pierwsze. Prawdopodobnie będziesz potrzebować pętli, aby upewnić się, że inne zadanie również się zakończyło. Zwykle chcesz wiedzieć, kiedy kończy się określone zadanie, co prowadzi do napisania kolejnych awaits.
Peter Ritchie,
5
Szczerość, BackgroundWorker nigdy nie był dobry dla operacji związanych z IO.
Peter Ritchie
21

To jest dobre wprowadzenie: http://msdn.microsoft.com/en-us/library/hh191443.aspx Sekcja Wątki jest właśnie tym, czego szukasz:

Metody asynchroniczne mają być operacjami nieblokującymi. Wyrażenie await w metodzie asynchronicznej nie blokuje bieżącego wątku, gdy oczekiwane zadanie jest uruchomione. Zamiast tego wyrażenie rejestruje pozostałą część metody jako kontynuację i zwraca kontrolę do obiektu wywołującego metodę asynchroniczną.

Słowa kluczowe async i await nie powodują tworzenia dodatkowych wątków. Metody asynchroniczne nie wymagają wielowątkowości, ponieważ metoda asynchroniczna nie działa we własnym wątku. Metoda działa w bieżącym kontekście synchronizacji i używa czasu w wątku tylko wtedy, gdy metoda jest aktywna. Możesz użyć Task.Run, aby przenieść pracę związaną z procesorem do wątku w tle, ale wątek w tle nie pomaga w procesie, który tylko czeka na udostępnienie wyników.

Oparte na asynchronii podejście do programowania asynchronicznego jest preferowane w prawie każdym przypadku. W szczególności to podejście jest lepsze niż BackgroundWorker w przypadku operacji związanych z we / wy, ponieważ kod jest prostszy i nie musisz chronić się przed warunkami wyścigu. W połączeniu z Task.Run programowanie asynchroniczne jest lepsze niż BackgroundWorker w przypadku operacji związanych z procesorem, ponieważ programowanie asynchroniczne oddziela szczegóły koordynacji uruchamiania kodu od pracy, którą Task.Run przekazuje do puli wątków.

TommyN
źródło
„w przypadku operacji związanych z IO, ponieważ kod jest prostszy i nie musisz się wystrzegać warunków wyścigu”. Jakie warunki wyścigu mogą wystąpić, czy możesz podać przykład?
eran otzap
8

BackgroundWorker jest jawnie oznaczony jako przestarzały w .NET 4.5:

Artykuł MSDN „Programowanie asynchroniczne przy użyciu funkcji Async and Await (C # i Visual Basic)” mówi:

Oparte na asynchronii podejście do programowania asynchronicznego jest preferowane w prawie każdym przypadku . W szczególności to podejście jest lepsze niż BackgroundWorker w przypadku operacji związanych z we / wy, ponieważ kod jest prostszy i nie musisz chronić się przed warunkami wyścigu. W połączeniu z Task.Run programowanie asynchroniczne jest lepsze niż BackgroundWorker w przypadku operacji związanych z procesorem, ponieważ programowanie asynchroniczne oddziela szczegóły koordynacji uruchamiania kodu od pracy, którą Task.Run przekazuje do puli wątków

AKTUALIZACJA

  • w odpowiedzi na komentarz @ eran-otzap :
    „dla operacji związanych z IO, ponieważ kod jest prostszy i nie musisz się wystrzegać warunków wyścigu”. Jakie warunki wyścigu mogą wystąpić, czy możesz podać przykład? "

To pytanie należało umieścić w osobnym poście.

Wikipedia dobrze wyjaśnia warunki wyścigów . Niezbędną częścią tego jest wielowątkowość i ten sam artykuł MSDN Asynchronous Programming with Async and Await (C # and Visual Basic) :

Metody asynchroniczne mają być operacjami nieblokującymi. Wyrażenie await w metodzie asynchronicznej nie blokuje bieżącego wątku, gdy oczekiwane zadanie jest uruchomione. Zamiast tego wyrażenie rejestruje pozostałą część metody jako kontynuację i zwraca kontrolę do obiektu wywołującego metodę asynchroniczną.

Słowa kluczowe async i await nie powodują tworzenia dodatkowych wątków. Metody asynchroniczne nie wymagają wielowątkowości, ponieważ metoda asynchroniczna nie działa we własnym wątku. Metoda działa w bieżącym kontekście synchronizacji i używa czasu w wątku tylko wtedy, gdy metoda jest aktywna. Możesz użyć Task.Run, aby przenieść pracę związaną z procesorem do wątku w tle, ale wątek w tle nie pomaga w procesie, który tylko czeka na udostępnienie wyników.

Oparte na asynchronii podejście do programowania asynchronicznego jest preferowane w prawie każdym przypadku. W szczególności to podejście jest lepsze niż BackgroundWorker w przypadku operacji związanych z we / wy, ponieważ kod jest prostszy i nie musisz chronić się przed warunkami wyścigu. W połączeniu z Task.Run programowanie asynchroniczne jest lepsze niż BackgroundWorker w przypadku operacji związanych z procesorem, ponieważ programowanie asynchroniczne oddziela szczegóły koordynacji uruchamiania kodu od pracy, którą Task.Run przekazuje do puli wątków

Oznacza to, że „Słowa kluczowe async i await nie powodują tworzenia dodatkowych wątków”.

O ile pamiętam moje własne próby, gdy studiowałem ten artykuł rok temu, jeśli uruchomiłeś i bawiłeś się próbką kodu z tego samego artykułu, możesz wpaść w sytuację, że jego wersje nie-asynchroniczne (możesz spróbować przekonwertować to do siebie) blokować na czas nieokreślony!

Ponadto, aby znaleźć konkretne przykłady, możesz przeszukać tę witrynę. Oto kilka przykładów:

Gennady Vanin Геннадий Ванин
źródło
30
BackgrondWorker nie jest jawnie oznaczony jako przestarzały w .NET 4.5. Artykuł MSDN mówi jedynie, że operacje związane z IO są lepsze w przypadku metod asynchronicznych - użycie BackgroundWorker nie oznacza, że ​​nie można używać metod asynchronicznych.
Peter Ritchie
@PeterRitchie, poprawiłem odpowiedź. Dla mnie „istniejące podejścia są przestarzałe” jest synonimem „Asynchroniczne podejście do programowania asynchronicznego jest w prawie każdym przypadku lepsze niż istniejące”
Gennady Vanin Геннадий Ванин
7
Mam problem z tą stroną MSDN. Po pierwsze, nie bardziej „koordynujesz” z BGW niż z Task. I tak, BGW nigdy nie było przeznaczone do bezpośredniego wykonywania operacji IO - zawsze istniał lepszy sposób na IO niż w BGW. Druga odpowiedź pokazuje, że BGW nie jest bardziej skomplikowany w użyciu niż Task. A jeśli prawidłowo używasz BGW, nie ma warunków wyścigu.
Peter Ritchie
„w przypadku operacji związanych z IO, ponieważ kod jest prostszy i nie musisz się wystrzegać warunków wyścigu”. Jakie warunki wyścigu mogą wystąpić, czy możesz podać przykład?
eran otzap
11
Ta odpowiedź jest błędna. Programowanie asynchroniczne może zbyt łatwo wywołać zakleszczenia w nietrywialnych programach. Dla porównania, BackgroundWorker jest prosty i solidny.
ZunTzu