Async / await vs BackgroundWorker

时间:2012-09-13 20:51:49

标签: c# task-parallel-library backgroundworker .net-4.5 async-await

在过去的几天里,我测试了.net 4.5和c#5的新功能。

我喜欢它的新async / await功能。之前我曾使用BackgroundWorker在后台处理响应式UI的更长进程。

我的问题是:在拥有这些不错的新功能之后,我何时应该使用async / await和BackgroundWorker?这两种情况的常见情况是什么?

5 个答案:

答案 0 :(得分:190)

这可能是TL;很多人都是DR,但是,我认为将awaitBackgroundWorker进行比较就像比较苹果和橘子以及我对此的看法:

BackgroundWorker用于模拟您希望在后台执行的单个任务,在线程池线程上。 async / await是异步等待异步操作的语法。这些操作可能使用或不使用线程池线程,甚至可以使用任何其他线程。所以,它们是苹果和橘子。

例如,您可以使用await执行以下操作:

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

但是,您可能永远不会在后台工作程序中对此进行建模,您可能会在.NET 4.0中执行此类操作(在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);

请注意两种语法之间的处置不相称以及如何在using / async之后无法使用await

但是,BackgroundWorker你不会做那样的事情。 BackgroundWorker通常用于建模您不希望影响UI响应的单个长时间运行操作。例如:

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

你可以使用async / await,BackgroundWorker正在为你创建线程。

现在,您可以改用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);

在这种情况下,TaskScheduler正在为您创建线程(假设默认为TaskScheduler),并且可以使用await,如下所示:

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"

在我看来,一个重要的比较是你是否报告了进展。例如,您可能有BackgroundWorker like这个:

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

但是,你不会处理其中一些因为你将后台工作组件拖放到表单的设计图面上 - 这是async /无法做到的事情。 awaitTask ...即您不会手动创建对象,设置属性并设置事件处理程序。您只需填写DoWorkRunWorkerCompletedProgressChanged事件处理程序的正文。

如果您将其“转换”为异步/等待,您可以执行以下操作:

     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"

如果没有将组件拖到Designer表面的能力,那么由读者决定哪个“更好”。但是,对我来说,这是awaitBackgroundWorker之间的比较,而不是您是否可以等待Stream.ReadAsync等内置方法。例如如果您按预期使用BackgroundWorker,则可能很难转换为使用await

其他想法:http://jeremybytes.blogspot.ca/2012/05/backgroundworker-component-im-not-dead.html

答案 1 :(得分:69)

async / await旨在替换BackgroundWorker等构造。虽然你肯定可以使用它,但你应该能够使用async / await和其他一些TPL工具来处理那里的一切。

由于两者都起作用,因此归结为个人偏好,以及您何时使用。 的速度更快? 更容易理解?

答案 2 :(得分:20)

这是一个很好的介绍:http://msdn.microsoft.com/en-us/library/hh191443.aspx 线程部分正是您所寻找的:

  

异步方法旨在实现非阻塞操作。在等待任务运行时,异步方法中的await表达式不会阻止当前线程。相反,表达式将方法的其余部分作为延续进行注册,并将控制权返回给异步方法的调用者。

     

async和await关键字不会导致创建其他线程。异步方法不需要多线程,因为异步方法不能在自己的线程上运行。该方法在当前同步上下文上运行,并仅在方法处于活动状态时在线程上使用时间。您可以使用Task.Run将受CPU限制的工作移动到后台线程,但后台线程对于只等待结果可用的进程没有帮助。

     

基于异步的异步编程方法几乎在所有情况下都优于现有方法。特别是,对于IO绑定操作,此方法优于BackgroundWorker,因为代码更简单,您无需防范竞争条件。与Task.Run结合使用,异步编程优于BackgroundWorker用于CPU绑定操作,因为异步编程将运行代码的协调细节与Task.Run传输到线程池的工作分开。

答案 3 :(得分:5)

BackgroundWorker在.NET 4.5中明确标记为过时:

MSDN文章"Asynchronous Programming with Async and Await (C# and Visual Basic)"告诉我们:

  

异步编程的基于异步的方法更可取   几乎在所有情况下都采用现有方法。特别是这个   对于IO绑定操作,方法优于BackgroundWorker   因为代码更简单,你不必防范种族   条件。结合Task.Run,​​异步编程更好   比BackgroundWorker 用于CPU绑定操作,因为异步   编程分离运行代码的协调细节   从 Task.Run 转移到线程池

的工作

<强>更新

  • 回应@eran-otzap的评论:
    “对于IO绑定操作,因为代码更简单,你不必防范竞争条件”可能会出现什么竞争条件,你能举个例子吗? “

这个问题应该作为一个单独的帖子。

维基百科对racing条件有很好的解释。 它的必要部分是多线程,并且来自同一篇MSDN文章Asynchronous Programming with Async and Await (C# and Visual Basic)

  

异步方法旨在实现非阻塞操作。等待   异步方法中的表达式不会阻塞当前线程   等待的任务正在运行。相反,表达式表示其余部分   该方法作为一个延续并将控制权返回给调用者   异步方法。

     

async和await关键字不会导致其他线程   创建。异步方法不需要多线程,因为异步   方法不会在自己的线程上运行。该方法在当前运行   同步上下文并仅在线程上使用时间   方法是有效的。您可以使用Task.Run将CPU绑定的工作移动到   后台线程,但后台线程对进程没有帮助   那只是等待结果可用。

     

基于异步的异步编程方法比较好   几乎在每种情况下现有的方法。特别是这种方法   对于IO绑定操作,它比BackgroundWorker更好,因为   代码更简单,您不必防范竞争条件。   与Task.Run结合使用,异步编程优于   BackgroundWorker用于CPU绑定操作,因为异步编程   将运行代码的协调细节与工作分开   Task.Run转移到线程池

即,“async和await关键字不会导致创建其他线程”。

据我一年前研究这篇文章时我记得自己的尝试,如果你运行并使用同一篇文章中的代码示例,你可能会遇到其非同步版本的情况(你可以尝试将其转换为自己)无限期阻止!

此外,对于具体示例,您可以搜索此站点。以下是一些例子:

答案 4 :(得分:1)

让我们在BackgroundWorkerTask.Run + Progress<T> + async/await组合之间进行最新比较。我将使用这两种方法来实现模拟的CPU绑定操作,该操作必须卸载到后台线程,以保持UI的响应速度。该操作的总持续时间为5秒,在操作过程中,ProgressBar必须每500毫秒更新一次。最后,计算结果必须显示在Label中。首先是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); // Simulate some time-consuming work
            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 = $"Result: {result:#,0}";
    };
    worker.RunWorkerAsync();
}
事件处理程序中的

24行代码。现在,让我们使用现代方法完全相同:

private async void Button_Click(object sender, EventArgs e)
{
    IProgress<int> progress = new Progress<int>(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); // Simulate some time-consuming work
            sum += i;
        }
        progress.Report(100);
        return sum;
    });
    Label1.Text = $"Result: {result:#,0}";
}
事件处理程序中的

17行代码。总体来说,代码要少得多。

在两种情况下,工作都是在ThreadPool线程上执行的。

BackgroundWorker方法的优点:

  1. 可以与针对.NET Framework 4.0及更早版本的项目一起使用。

Task.Run + Progress<T> + async / await方法的优点:

  1. 结果是强类型的。无需从object进行投射。没有运行时InvalidCastException的风险。
  2. 工作完成后的延续在原始范围内运行,而不是在lamda内部运行。
  3. 允许通过Progress报告任意强类型信息。相反,BackgroundWorker会强制您以object的形式传递任何额外的信息,然后从object ProgressChangedEventArgs.UserState属性进行回退。
  4. 允许使用多个Progress对象,轻松地报告具有不同频率的不同进度数据。 BackgroundWorker非常繁琐且容易出错。
  5. 取消操作遵循standard .NET pattern for cooperative cancellationCancellationTokenSource + CancellationToken组合键。 当前,有成千上万个消耗CancellationToken的.NET API。相反,BackgroundWorker的取消机制无法使用,因为它不会生成通知。
  6. 最后,Task.Run同样轻松地支持同步和异步工作负载。 BackgroundWorker只能通过阻止工作线程来使用异步API。