190 votes

Async/await vs BackgroundWorker

Au cours des derniers jours, j'ai testé les nouvelles fonctionnalités de .net 4.5 et de c# 5.

J'aime ses nouvelles fonctionnalités async/await. Auparavant, j'avais utilisé BackgroundWorker pour gérer des processus plus longs en arrière-plan tout en conservant une interface utilisateur réactive.

Ma question est la suivante : après avoir ces nouvelles fonctionnalités intéressantes, quand devrais-je utiliser async/await et quand un BackgroundWorker? Quels sont les scénarios courants pour chacun?

0 votes

0 votes

Les deux sont bons, mais si vous travaillez avec du code plus ancien qui n'a pas été migré vers une version ultérieure .net ; BackgroundWorker fonctionne sur les deux.

236voto

Peter Ritchie Points 18352

Ceci est probablement trop long à lire pour beaucoup d'entre vous, mais je pense que comparer await avec BackgroundWorker revient à comparer des pommes et des oranges et mes réflexions à ce sujet suivent :

BackgroundWorker est destiné à modéliser une tâche unique que vous souhaitez exécuter en arrière-plan, sur un thread du pool de threads. async/await est une syntaxe pour attendre de manière asynchrone sur des opérations asynchrones. Ces opérations peuvent ou non utiliser un thread pool ou même utiliser n'importe quel autre thread. Ainsi, ils sont comme des pommes et des oranges.

Par exemple, vous pouvez faire quelque chose comme ce qui suit avec await :

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

Mais vous ne modéliseriez probablement jamais cela dans un worker en arrière-plan, vous feriez probablement quelque chose comme ceci en .NET 4.0 (avant 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);

Remarquez la différence de gestion de la libération entre les deux syntaxes et le fait que vous ne pouvez pas utiliser using sans async/await.

Mais, vous ne feriez pas quelque chose comme ça avec BackgroundWorker. BackgroundWorker est généralement destiné à modéliser une seule opération de longue durée que vous ne voulez pas impacter la réactivité de l'interface utilisateur. Par exemple :

worker.DoWork += (sender, e) =>
                    {
                    int i = 0;
                    // simuler une opération de longue durée
                    Stopwatch sw = Stopwatch.StartNew();
                    while (sw.Elapsed.TotalSeconds < 1)
                        ++i;
                    };
worker.RunWorkerCompleted += (sender, eventArgs) =>
                                {
                                    // À FAIRE : quelque chose sur le thread de l'interface utilisateur, comme
                                    // mettre à jour le statut ou afficher le "résultat"
                                };
worker.RunWorkerAsync();

Il n'y a vraiment rien là où vous pourriez utiliser async/await, BackgroundWorker crée le thread pour vous.

Maintenant, vous pourriez utiliser le TPL à la place :

var synchronizationContext = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() =>
                      {
                        int i = 0;
                        // simuler une opération de longue durée
                        Stopwatch sw = Stopwatch.StartNew();
                        while (sw.Elapsed.TotalSeconds < 1)
                            ++i;
                      }).ContinueWith(t=>
                                      {
                                        // À FAIRE : quelque chose sur le thread de l'interface utilisateur, comme
                                        // mettre à jour le statut ou afficher le "résultat"
                                      }, synchronizationContext);

Dans ce cas, le TaskScheduler crée le thread pour vous (en supposant le TaskScheduler par défaut), et pourrait utiliser await comme suit :

await Task.Factory.StartNew(() =>
                  {
                    int i = 0;
                    // simuler une opération de longue durée
                    Stopwatch sw = Stopwatch.StartNew();
                    while (sw.Elapsed.TotalSeconds < 1)
                        ++i;
                  });
// À FAIRE : quelque chose sur le thread de l'interface utilisateur, comme
// mettre à jour le statut ou afficher le "résultat"

À mon avis, une comparaison importante concerne la gestion de la progression ou non. Par exemple, vous pourriez avoir un BackgroundWorker comme ceci :

BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.ProgressChanged += (sender, eventArgs) =>
                            {
                            // À FAIRE : quelque chose avec la progression, comme mettre à jour la barre de progression

                            };
worker.DoWork += (sender, e) =>
                 {
                    int i = 0;
                    // simuler une opération de longue durée
                    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) =>
                                {
                                    // Faire quelque chose sur le thread de l'interface utilisateur, comme
                                    // mettre à jour le statut ou afficher le "résultat"
                                };
worker.RunWorkerAsync();

Mais vous ne traiteriez pas certaines de ces choses parce que vous glisseriez-déposeriez le composant worker en arrière-plan sur la surface de design d'un formulaire - quelque chose que vous ne pouvez pas faire avec async/await et Task... c'est-à-dire que vous ne créez pas manuellement l'objet, ne définissez pas les propriétés et ne définissez pas les gestionnaires d'événements. vous rempliriez seulement le corps des gestionnaires d'événements DoWork, RunWorkerCompleted et ProgressChanged.

Si vous "convertissiez" cela en async/await, vous feriez quelque chose comme :

     IProgress progress = new Progress();

     progress.ProgressChanged += ( s, e ) =>
        {
           // À FAIRE : quelque chose avec e.ProgressPercentage
           // comme mettre à jour la barre de progression
        };

     await Task.Factory.StartNew(() =>
                  {
                    int i = 0;
                    // simuler une opération de longue durée
                    Stopwatch sw = Stopwatch.StartNew();
                    while (sw.Elapsed.TotalSeconds < 1)
                    {
                        if ((sw.Elapsed.TotalMilliseconds%100) == 0)
                        {
                            progress.Report((int) (1000 / sw.ElapsedMilliseconds))
                        }
                        ++i;
                    }
                  });
// À FAIRE : quelque chose sur le thread de l'interface utilisateur, comme
// mettre à jour le statut ou afficher le "résultat"

Sans la possibilité de glisser un composant sur une surface de design, c'est vraiment à vous de décider lequel est "meilleur". Mais, pour moi, c'est là la comparaison entre await et BackgroundWorker, non pas si vous pouvez attendre des méthodes intégrées comme Stream.ReadAsync. par exemple, si vous utilisez BackgroundWorker comme prévu, il pourrait être difficile de le convertir pour utiliser await.

Autres réflexions : http://jeremybytes.blogspot.ca/2012/05/backgroundworker-component-im-not-dead.html

4 votes

Une faille que je pense existe avec async / await est que vous pouvez souhaiter démarrer plusieurs tâches asynchrones en même temps. await est censé attendre que chaque tâche se termine avant de démarrer la suivante. Et si vous omettez le mot-clé await, la méthode s'exécute de manière synchrone ce qui n'est pas ce que vous voulez. Je ne pense pas que async / await puisse résoudre un problème tel que "démarrer ces 5 tâches et me rappeler lorsque chaque tâche est terminée dans un ordre aléatoire".

5 votes

@Moozhe. Pas vrai, vous pouvez faire var t1 = webReq.GetResponseAsync(); var t2 = webReq2.GetResponseAsync(); await t1; await t2;. Ce qui attendrait deux opérations en parallèle. Attendre est bien meilleur pour les tâches asynchrones, mais séquentielles, à mon avis...

1 votes

Vous avez raison, mais j'ai dû omettre d'utiliser le mot-clé await pour le faire. Utiliser await comme dans votre exemple force les rappels à se produire dans l'ordre que vous spécifiez, ce qui signifie que même si t2 se termine avant t1, il n'exécutera pas son rappel tant que t1 n'aura pas fini.

80voto

Servy Points 93720

Async/await est conçu pour remplacer des constructions telles que le BackgroundWorker. Bien sûr, vous pouvez l'utiliser si vous le souhaitez, mais vous devriez pouvoir utiliser async/await, ainsi que quelques autres outils TPL, pour gérer tout ce qui existe.

Comme les deux fonctionnent, tout dépend de vos préférences personnelles quant à celui que vous utilisez quand. Qu'est-ce qui est plus rapide pour vous? Qu'est-ce qui est plus facile à comprendre pour vous?

20 votes

Merci. Pour moi, async/await semble beaucoup plus clair et 'naturel'. BakcgoundWorker rend le code plus 'bruyant' à mon avis.

12 votes

@Tom Eh bien, c'est pourquoi Microsoft a passé beaucoup de temps et d'efforts à le mettre en œuvre. S'il n'était pas meilleur, ils n'auraient pas pris la peine.

5 votes

Oui. Le nouvel élément `await` rend l'ancien BackgroundWorker complètement inférieur et obsolète. La différence est si dramatique.

25voto

TommyN Points 548

Ceci est une bonne introduction: http://msdn.microsoft.com/en-us/library/hh191443.aspx La section Threads est exactement ce que vous recherchez:

Les méthodes asynchrones sont conçues pour être des opérations non bloquantes. Une expression await dans une méthode asynchrone ne bloque pas le thread actuel pendant que la tâche attendue s'exécute. Au lieu de cela, l'expression enregistre le reste de la méthode comme une continuation et retourne le contrôle à l'appelant de la méthode asynchrone.

Les mots clés async et await ne créent pas de threads supplémentaires. Les méthodes asynchrones ne nécessitent pas de multithreading car une méthode asynchrone ne s'exécute pas sur son propre thread. La méthode s'exécute sur le contexte de synchronisation actuel et utilise du temps sur le thread uniquement lorsque la méthode est active. Vous pouvez utiliser Task.Run pour déplacer le travail lié au CPU vers un thread en arrière-plan, mais un thread en arrière-plan n'aide pas avec un processus qui attend simplement que les résultats deviennent disponibles.

L'approche basée sur l'asynchronisme pour la programmation asynchrone est préférable aux approches existantes dans presque tous les cas. En particulier, cette approche est meilleure que BackgroundWorker pour les opérations liées aux E/S car le code est plus simple et vous n'avez pas besoin de protéger contre les conditions de concurrence. En combinaison avec Task.Run, la programmation asynchrone est meilleure que BackgroundWorker pour les opérations liées au CPU car la programmation asynchrone sépare les détails de coordination de l'exécution de votre code du travail que Task.Run transfère au pool de threads.

0 votes

"pour les opérations liées à l'IO car le code est plus simple et vous n'avez pas à vous prémunir contre les conditions de course" Quelles conditions de course peuvent survenir, pourriez-vous donner un exemple ?

13voto

BackgroundWorker est explicitement étiqueté comme obsolète dans .NET 4.5:

L'article de MSDN "Programmation asynchrone avec Async et Await (C# et Visual Basic)" indique :

L'approche basée sur async pour la programmation asynchrone est préférable aux approches existantes dans presque tous les cas. En particulier, cette approche est meilleure que BackgroundWorker pour les opérations liées à l'IO car le code est plus simple et vous n'avez pas à vous protéger contre les conditions de course. En combinaison avec Task.Run, la programmation asynchrone est meilleure que BackgroundWorker pour les opérations liées au CPU car la programmation asynchrone sépare les détails de coordination de l'exécution de votre code du travail que Task.Run transfère au threadpool

MISE À JOUR

  • en réponse au commentaire de @eran-otzap:
    "pour les opérations liées à l'IO car le code est plus simple et vous n'avez pas à vous protéger contre les conditions de course". Quelles conditions de course peuvent survenir, pourriez-vous donner un exemple ? "

Cette question aurait dû être posée comme un post séparé.

Wikipedia donne une bonne explication des conditions de course. La partie nécessaire est celle de la multithreading et de l'article MSDN Programmation asynchrone avec Async et Await (C# et Visual Basic):

Les méthodes asynchrones sont censées être des opérations non bloquantes. Une expression await dans une méthode async ne bloque pas le fil d'exécution actuel pendant que la tâche attendue s'exécute. Au contraire, l'expression inscrit le reste de la méthode en tant que continuation et retourne le contrôle à l'appelant de la méthode async.

Les mots-clés async et await ne créent pas de threads supplémentaires. Les méthodes asynchrones ne nécessitent pas de multiprocessus car une méthode async ne s'exécute pas sur son propre fil. La méthode s'exécute sur le contexte de synchronisation actuel et utilise le temps sur le fil uniquement lorsque la méthode est active. Vous pouvez utiliser Task.Run pour déplacer le travail lié au CPU vers un fil de fond, mais un fil de fond ne vous aide pas avec un processus qui attend juste que les résultats soient disponibles.

L'approche basée sur async pour la programmation asynchrone est préférable aux approches existantes dans presque tous les cas. En particulier, cette approche est meilleure que BackgroundWorker pour les opérations liées à l'IO car le code est plus simple et vous n'avez pas à vous protéger contre les conditions de course. En combinaison avec Task.Run, la programmation asynchrone est meilleure que BackgroundWorker pour les opérations liées au CPU car la programmation asynchrone sépare les détails de coordination de l'exécution de votre code du travail que Task.Run transfère au threadpool

C'est-à-dire, "Les mots-clés async et await ne créent pas de threads supplémentaires".

En ce qui concerne mes propres tentatives lorsque j'étudiais cet article il y a un an, si vous avez exécuté et joué avec l'échantillon de code de l'article, vous pourriez vous retrouver dans une situation où ses versions non asynchrones (vous pourriez essayer de les convertir vous-même) bloquent indéfiniment !

De plus, pour des exemples concrets, vous pouvez rechercher sur ce site. Voici quelques exemples :

37 votes

BackgroundWorker n'est pas explicitement étiqueté comme obsolète dans .NET 4.5. L'article MSDN se contente de dire que les opérations liées à l'entrée/sortie sont meilleures avec des méthodes asynchrones - l'utilisation de BackgroundWorker ne signifie pas que vous ne pouvez pas utiliser des méthodes asynchrones.

0 votes

@PeterRitchie , j'ai corrigé ma réponse. Pour moi, "existing approaches are obsolete" est synonyme de "L'approche basée sur l'asynchrone à la programmation asynchrone est préférable aux approches existantes dans presque tous les cas"

9 votes

Je conteste cette page MSDN. Pour commencer, vous ne faites pas plus de "coordination" avec BGW qu'avec Task. De plus, oui, BGW n'a jamais été conçu pour effectuer directement des opérations d'E/S - il a toujours été un meilleur moyen de faire de l'E/S qu'avec BGW. L'autre réponse montre que BGW n'est pas plus complexe à utiliser que Task. Et si vous utilisez BGW correctement, il n'y a pas de conditions de concurrence.

11voto

Theodor Zoulias Points 1088

Faisons une comparaison actualisée entre un BackgroundWorker et la combinaison Task.Run + Progress + async/await. Je vais utiliser les deux approches pour implémenter une opération liée au CPU qui doit être déchargée vers un thread d'arrière-plan pour garder l'interface utilisateur réactive. L'opération a une durée totale de 5 secondes, et pendant l'opération une ProgressBar doit être mise à jour toutes les 500 ms. Enfin, le résultat du calcul doit être affiché dans un Label. Commençons par l'implémentation du BackgroundWorker:

private void Button_Click(object sender, EventArgs e)
{
    var worker = new BackgroundWorker();
    worker.WorkerReportsProgress = true;
    worker.DoWork += (object sender, DoWorkEventArgs e) =>
    {
        int sum = 0;
        for (int i = 0; i < 100; i += 10)
        {
            worker.ReportProgress(i);
            Thread.Sleep(500); // Simuler un travail long
            sum += i;
        }
        worker.ReportProgress(100);
        e.Result = sum;
    };
    worker.ProgressChanged += (object sender, ProgressChangedEventArgs e) =>
    {
        ProgressBar1.Value = e.ProgressPercentage;
    };
    worker.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e) =>
    {
        int result = (int)e.Result;
        Label1.Text = $"Résultat : {result:#,0}";
    };
    worker.RunWorkerAsync();
}

24 lignes de code à l'intérieur du gestionnaire d'événements. Maintenant, faisons exactement la même chose avec l'approche moderne :

private async void Button_Click(object sender, EventArgs e)
{
    IProgress progress = new Progress(percent =>
    {
        ProgressBar1.Value = percent;
    });
    int result = await Task.Run(() =>
    {
        int sum = 0;
        for (int i = 0; i < 100; i += 10)
        {
            progress.Report(i);
            Thread.Sleep(500); // Simuler un travail long
            sum += i;
        }
        progress.Report(100);
        return sum;
    });
    Label1.Text = $"Résultat : {result:#,0}";
}

17 lignes de code à l'intérieur du gestionnaire d'événements. Beaucoup moins de code au total.

Dans les deux cas, le travail est exécuté sur un thread du ThreadPool.

Avantages de l'approche du BackgroundWorker :

  1. Peut être utilisé avec des projets ciblant le .NET Framework 4.0 et les versions antérieures.

Avantages de l'approche du Task.Run + Progress + async/await :

  1. Le résultat est fortement typé. Pas besoin de le convertir d'un object. Aucun risque de InvalidCastException à l'exécution.
  2. La continuation après l'achèvement du travail s'exécute dans le contexte d'origine, pas à l'intérieur d'une lamda.
  3. Permet de rapporter des informations fortement typées arbitraires à travers le Progress. En revanche, un BackgroundWorker vous oblige à passer des informations supplémentaires en tant qu'object, puis à les reconvertir à partir de la propriété ProgressChangedEventArgs.UserState de l'object.
  4. Permet d'utiliser plusieurs objets Progress pour rapporter des données de progression différentes avec différentes fréquences facilement. C'est très fastidieux et sujet aux erreurs avec un BackgroundWorker.
  5. Annuler l'opération suit le modèle standard de .NET pour l'annulation coopérative : le combo CancellationTokenSource + CancellationToken. Actuellement, des milliers d'API .NET consomment un CancellationToken. En revanche, le mécanisme d'annulation du BackgroundWorker ne peut pas être consommé car il ne génère pas de notifications.
  6. Enfin, le Task.Run prend en charge à la fois les charges de travail synchrones et asynchrones avec la même facilité. Le BackgroundWorker ne peut consommer des API asynchrones qu'en bloquant le thread de travail.

1 votes

Au fait, j'ai récemment pris conscience que Stephen Cleary a publié une série de billets de blog sur le même thème.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X