异步/等待 VS 后台工作者

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

我喜欢它的新的异步/等待特性。早些时候,我曾使用 背景工作者处理具有响应 UI 的后台较长进程。

我的问题是: 在拥有这些优秀的新特性之后,什么时候应该使用异步/等待,什么时候应该使用 背景工作者?这两种情况下常见的情况是什么?

108699 次浏览

Sync/wait 被设计用于替换诸如 BackgroundWorker之类的构造。当然,如果您愿意,可以也可以使用它,但是您应该能够使用异步/等待,以及其他一些 TPL 工具来处理所有的事情。

因为这两种工作,它归结为个人的偏好,你使用的时间。什么是 更快?更容易理解什么?

这是一个很好的介绍: < a href = “ http://msdn.microsoft.com/en-us/library/hh191443.aspx”rel = “ noReferrer”> http://msdn.microsoft.com/en-us/library/hh191443.aspx Threads 部分正是您所需要的:

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

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

在几乎所有情况下,基于异步的异步编程方法都优于现有方法。尤其是,对于 IO 绑定操作,这种方法比 BackoundWorker 更好,因为代码更简单,而且不需要防止竞争条件。结合任务。运行时,异步编程在 CPU 绑定操作方面优于 BackoundWorker,因为异步编程将运行代码的协调细节与 Task 的工作分离开来。运行传输到线程池。

对于很多人来说,这可能是 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();

实际上那里没有什么可以使用异步/等待的东西,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

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

MSDN 文章 “使用异步和等待进行异步编程(C # 和 VisualBasic)”告诉我们:

基于异步的异步编程方法 < strong > 更可取 在几乎所有的情况下,对现有的方法都是 方法优于 背景工作者 用于 IO 绑定操作 因为代码更简单,而且不需要防止种族歧视 结合 Task.Run,异步编程更好 因为异步 编程将运行代码的协调细节分离开来 从 任务,快跑传输到线程池的工作

更新

  • 针对 @ eran-otzap的评论:
    “用于 IO 绑定操作,因为代码更简单,而且不需要防止竞态条件”“可以发生什么竞态条件,您能举个例子吗?”

这个问题应该单独提出来。

Wikipedia 对 赛车条件有很好的解释。 它的必要部分是多线程和来自同一 MSDN 文章 使用异步和等待进行异步编程(C # 和 VisualBasic)的:

异步方法应该是非阻塞操作 异步方法中的表达式不会阻塞当前线程,而 等待的任务正在运行。相反,表达式签署了其余的任务 方法的调用者,并将控制返回给 异步方法。

异步和等待关键字不会导致额外的线程 异步方法不需要多线程,因为异步 方法不在自己的线程上运行。该方法在当前 同步上下文,并仅当 方法处于活动状态。您可以使用 Task 后台线程,但是后台线程对进程没有帮助 只是在等结果出来。

基于异步的异步编程方法优于 在几乎所有的情况下,现有的方法。特别是,这种方法 在 IO 绑定操作方面优于 BackoundWorker,因为 代码更简单,而且不需要防止竞态条件。 结合 Task.Run,异步编程比 用于 CPU 绑定操作的 BackoundWorker,因为异步编程 将运行代码的协调细节与工作分离开来 运行传输到线程池

也就是说,“异步和等待关键字不会导致创建额外的线程”。

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

另外,你可以在这个网站上搜索一些具体的例子。下面是一些例子:

让我们对 BackgroundWorkerTask.Run + Progress<T> + 异步/等待组合做一个最新的比较。我将使用这两种方法来实现一个模拟的 CPU 绑定操作,该操作必须卸载到后台线程,以保持 UI 响应。该操作的总持续时间为5秒,在操作期间,必须每500毫秒更新一次 ProgressBar。最后,计算结果必须显示在 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. 工作完成后的延续是在原来的范围内运行,而不是在一个喇嘛。
  3. 允许通过 Progress报告任意强类型的信息。相反,BackgroundWorker强制您将任何额外的信息作为 object传递,然后从 object ProgressChangedEventArgs.UserState属性强制转换回来。
  4. 允许使用多个 Progress对象,以不同的频率报告不同的进度数据,方便。对于 BackgroundWorker,这是非常乏味和容易出错的。
  5. 取消操作遵循 用于协作取消的标准.NET 模式: CancellationTokenSource + CancellationToken组合。 目前有成千上万。使用 CancellationToken的 NET API。相反,不能使用 BackgroundWorkers 取消机制,因为它不生成通知。
  6. 最后,Task.Run同样轻松地支持同步和异步工作负载。BackgroundWorker只能通过阻塞辅助线程来使用异步 API。