异步/等待与 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,但是,我认为比较 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);
请注意两种语法之间的处理不相交,以及如何在没有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
... 即您不会手动创建对象、设置属性和设置事件处理程序。您只需填写 DoWork
、RunWorkerCompleted
和 ProgressChanged
事件处理程序的正文。
如果您将其“转换”为 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 界面上,则真正由读者决定哪个“更好”。但是,对我来说,这是await
和BackgroundWorker
之间的比较,而不是您是否可以等待像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)
在任一任务首先完成时执行某些操作。您可能需要一个循环来确保其他任务也完成。通常您想知道特定任务何时完成,这会导致您编写顺序await
s。
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】:让我们在 BackgroundWorker
和 Task.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
方法的优点:
-
可用于以.NET Framework 4.0 及更早版本为目标的项目。
Task.Run
+ Progress<T>
+ async
/await
方法的优点:
-
结果是强类型的。无需从
object
投射。没有运行时的风险InvalidCastException
。
工作完成后的延续是在原来的范围内运行,而不是在一个lamda里面。
允许通过Progress
报告任意强类型信息。相反,BackgroundWorker
会强制您将任何额外信息作为object
传递,然后从object
ProgressChangedEventArgs.UserState
属性中回滚。
允许使用多个Progress
对象,以不同的频率报告不同的进度数据,轻松。使用BackgroundWorker
非常繁琐且容易出错。
取消操作遵循standard .NET pattern for cooperative cancellation:CancellationTokenSource
+ CancellationToken
组合。
目前有数千个使用CancellationToken
的.NET API。相反,BackgroundWorker
s 取消机制无法使用,因为它不会生成通知。
最后,Task.Run
同样轻松地支持同步和异步工作负载。 BackgroundWorker
只能通过阻塞工作线程来使用异步 API。
【讨论】:
顺便说一句,最近我发现 Stephen Cleary 在同一主题上发布了 series of blog posts。以上是关于异步/等待与 BackgroundWorker的主要内容,如果未能解决你的问题,请参考以下文章