186

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

我喜欢它的新异步/等待功能。早些时候,我使用BackgroundWorker来处理具有响应式 UI 的后台较长进程。

我的问题是:在有了这些不错的新功能之后,我应该什么时候使用 async/await 以及什么时候使用BackgroundWorker?两者的常见情况是什么?

4

5 回答 5

229

对于许多人来说,这可能是 TL;DR,但是,我认为比较await就像BackgroundWorker比较苹果和橙子,我对此的想法如下:

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将为您创建线程(假设为 default 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... 做的事情,即您不会t 手动创建对象、设置属性和设置事件处理程序。您只需填写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

于 2012-09-13T22:29:41.563 回答
79

async/await 旨在替换诸如BackgroundWorker. 虽然您当然可以根据需要使用它,但您应该能够使用 async/await 以及其他一些 TPL 工具来处理所有可用的东西。

由于两者都有效,因此取决于您何时使用的个人偏好。什么对你来说更快?更容易理解什么?

于 2012-09-13T20:55:11.517 回答
23

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

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

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

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

于 2012-09-13T21:30:15.353 回答
11

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

MSDN 文章“使用 Async 和 Await 进行异步编程(C# 和 Visual Basic)”讲述:

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

更新

  • 回应@eran-otzap的评论:
    “对于 IO 绑定操作,因为代码更简单,而且您不必防范竞争条件”可能发生哪些竞争条件,您能举个例子吗?"

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

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

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

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

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

也就是说,“async 和 await 关键字不会导致创建额外的线程”。

据我记得一年前我在学习这篇文章时自己的尝试,如果你已经运行并玩过同一篇文章中的代码示例,你可能会遇到它的非异步版本(你可以尝试转换它给自己)无限期地阻止!

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

于 2013-05-03T19:43:31.660 回答
8

让我们对aBackgroundWorkerTask.Run++ async/await组合进行最新比较。我将使用这两种方法来实现必须卸载到后台线程的模拟 CPU 绑定操作,以保持 UI 响应。该操作的总持续时间为 5 秒,并且在操作期间必须每 500 毫秒更新一次。最后,计算结果必须显示在. 首先是实现:Progress<T>ProgressBarLabelBackgroundWorker

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>asyncawait

  1. 结果是强类型的。无需从object. 没有运行时的风险InvalidCastException
  2. 工作完成后的延续是在原来的范围内运行,而不是在一个lamda里面。
  3. 允许通过Progress. 相反, aBackgroundWorker迫使您将任何额外信息作为 传递object,然后从object ProgressChangedEventArgs.UserState属性中回退。
  4. 允许使用多个Progress对象,以不同的频率报告不同的进度数据,轻松。这是非常乏味且容易出错的BackgroundWorker.
  5. 取消操作遵循协作取消的标准 .NET 模式CancellationTokenSource+CancellationToken组合。目前有数千个 .NET API 使用CancellationToken. 相反,BackgroundWorkers 取消机制不能被使用,因为它不生成通知。
  6. 最后Task.Run,它同样轻松地支持同步和异步工作负载。BackgroundWorker只能通过阻塞工作线程来使用异步 API 。
于 2020-10-31T10:51:28.503 回答