异步/等待与 BackgroundWorker

Posted

技术标签:

【中文标题】异步/等待与 BackgroundWorker【英文标题】:Async/await vs BackgroundWorker 【发布时间】:2021-09-19 03:14:30 【问题描述】:

这几天我测试了.net 4.5和c#5的新特性。

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

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

【问题讨论】:

另见***.com/questions/3513432/…和***.com/questions/4054263/… 两者都很好,但如果您使用尚未迁移到更高 .net 版本的旧代码; BackgroundWorker 适用于两者。 【参考方案1】:

这对许多人来说可能是 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);

请注意两种语法之间的处理不相交,以及如何在没有async/await 的情况下使用using

但是,你不会对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/@987654347 做的事情@ 和Task... 即您不会手动创建对象、设置属性和设置事件处理程序。您只需填写 DoWorkRunWorkerCompletedProgressChanged 事件处理程序的正文。

如果您将其“转换”为 async/await,您会执行以下操作:

     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

【讨论】:

我认为 async/await 存在的一个缺陷是您可能希望一次启动多个异步任务。 await 意味着在开始下一个任务之前等待每个任务完成。如果您省略 await 关键字,则该方法会同步运行,这不是您想要的。我不认为 async/await 可以解决诸如“启动这 5 个任务并在每个任务没有特定顺序完成时给我回电”之类的问题。 @Moozhe。不是真的,你可以做var t1 = webReq.GetResponseAsync(); var t2 = webReq2.GetResponseAsync(); await t1; await t2;。这将等待两个并行操作。 Await 对于异步但顺序的任务要好得多,IMO... @Moozhe 是的,这样做可以保持一定的顺序——正如我所提到的。这是 await 的主要目的是在看起来顺序的代码中获得异步性。当然,您可以使用await Task.WhenAny(t1, t2) 在任一任务首先完成时执行某些操作。您可能需要一个循环来确保其他任务也完成。通常您想知道特定任务何时完成,这会导致您编写顺序awaits。 Wasn't it .NET 4.0 TPL that made APM, EAP and BackgroundWorker asynchronous patterns obsolete? 还是一头雾水 诚实,BackgroundWorker 从来没有适合 IO 密集型操作。【参考方案2】:

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

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

【讨论】:

谢谢。对我来说 async/await 似乎更加清晰和“自然”。在我看来,BakcgoundWorker 使代码“嘈杂”。 @Tom 嗯,这就是微软花费 大量 时间和精力来实现它的原因。如果不是更好,他们就不会打扰 是的。新的 await 东西使旧的 BackgroundWorker 看起来完全低劣和过时。不同之处在于戏剧性。 我有一个很好的概要on my blog 比较不同的后台任务方法。注意async / await 也允许异步编程没有线程池线程。 否决了这个答案,这是一种误导。 Async/await 并非旨在取代后台工作人员。【参考方案3】:

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

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

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

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

【讨论】:

"用于 IO-bound 操作,因为代码更简单,您不必防范竞争条件" 可能发生哪些竞争条件,您能举个例子吗?【参考方案4】:

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

在书中By Joseph Albahari, Ben Albahari "C# 5.0 in a Nutshell: The Definitive Reference" Stephen Cleary 回复my question "Wasn't it .NET 4.0 TPL that made APM, EAP and BackgroundWorker asynchronous patterns obsolete?"

MSDN 文章"Asynchronous Programming with Async and Await (C# and Visual Basic)" 讲述:

基于异步的异步编程方法更可取 几乎在所有情况下都采用现有方法。特别是,这 方法优于 BackgroundWorker对于 IO 绑定操作 因为代码更简单,你不必提防种族 状况。结合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 关键字不会导致创建额外的线程”。

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

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

Calling an async method with c#5.0 Why does this code fail when executed via TPL/Tasks? await vs Task.Wait - Deadlock?

【讨论】:

BackgrondWorker 在 .NET 4.5 中被明确标记为已过时。 MSDN 文章只是说使用异步方法更好地执行 IO 绑定操作——使用 BackgroundWorker 并不意味着您不能使用异步方法。 @PeterRitchie,我更正了我的答案。对我来说,“现有方法已经过时”是“在几乎所有情况下,基于异步的异步编程方法都比现有方法更可取”的同义词 我对那个 MSDN 页面有异议。一方面,您与 BGW 的“协调”并不比您与 Task 的“协调”多。而且,是的,BGW 从未打算直接执行 IO 操作——总是比在 BGW 中执行 IO 的方式更好。另一个答案表明 BGW 的使用并不比 Task 复杂。如果你正确使用 BGW,就不会有竞争条件。 "用于 IO 绑定操作,因为代码更简单,您不必防范竞争条件" 可能发生哪些竞争条件,您能举个例子吗? 这个答案是错误的。异步编程很容易在非平凡的程序中触发死锁。相比之下,BackgroundWorker 简单且坚如磐石。【参考方案5】:

让我们在 BackgroundWorkerTask.Run + Progress&lt;T&gt; + 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 方法的优点:

    可用于以.NET Framework 4.0 及更早版本为目标的项目。

Task.Run + Progress&lt;T&gt; + async/await 方法的优点:

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

【讨论】:

顺便说一句,最近我发现 Stephen Cleary 在同一主题上发布了 series of blog posts。

以上是关于异步/等待与 BackgroundWorker的主要内容,如果未能解决你的问题,请参考以下文章

如何将异步等待与 https 发布请求一起使用

Node.js 与 .net 中的异步/等待

异步/等待线程安全与内存缓存

C# 理解阻塞 UI 和异步/等待与 Task.Run 的问题?

java交互方式中的同步与异步

javascript 与异步等待的承诺