我试图更好地理解我在C#中的Async和Parallel选项。在下面的片段中,我列出了我最常遇到的5种方法。但我不确定选择哪个 - 或者更好的是,在选择时要考虑的标准:
方法1:任务
(见http://msdn.microsoft.com/en-us/library/dd321439.aspx)
调用StartNew在功能上等同于使用其构造函数之一创建Task,然后调用Start来安排执行。但是,除非必须分离创建和调度,否则StartNew是简化和性能的推荐方法。
TaskFactory的StartNew方法应该是创建和调度计算任务的首选机制,但是对于必须分离创建和调度的场景,可以使用构造函数,然后可以使用任务的Start方法来调度任务在以后执行。
// using System.Threading.Tasks.Task.Factory
void Do_1()
{
var _List = GetList();
_List.ForEach(i => Task.Factory.StartNew(_ => { DoSomething(i); }));
}
方法2:QueueUserWorkItem
(见http://msdn.microsoft.com/en-us/library/system.threading.threadpool.getmaxthreads.aspx)
您可以将系统内存允许的线程池请求排队。如果请求多于线程池线程,则其他请求将保持排队,直到线程池线程可用为止。
您可以将排队方法所需的数据放在定义方法的类的实例字段中,也可以使用接受包含必要数据的对象的QueueUserWorkItem(WaitCallback,Object)重载。
// using System.Threading.ThreadPool
void Do_2()
{
var _List = GetList();
var _Action = new WaitCallback((o) => { DoSomething(o); });
_List.ForEach(x => ThreadPool.QueueUserWorkItem(_Action));
}
方法3:Parallel.Foreach
(见:http://msdn.microsoft.com/en-us/library/system.threading.tasks.parallel.foreach.aspx)
Parallel类为常见操作提供基于库的数据并行替换,例如for循环,每个循环以及执行一组语句。
对源枚举中的每个元素调用一次body委托。它以当前元素作为参数提供。
// using System.Threading.Tasks.Parallel
void Do_3()
{
var _List = GetList();
var _Action = new Action<object>((o) => { DoSomething(o); });
Parallel.ForEach(_List, _Action);
}
方法4:IAsync.BeginInvoke
(见:http://msdn.microsoft.com/en-us/library/cc190824.aspx)
BeginInvoke是异步的;因此,控制在调用后立即返回调用对象。
// using IAsync.BeginInvoke()
void Do_4()
{
var _List = GetList();
var _Action = new Action<object>((o) => { DoSomething(o); });
_List.ForEach(x => _Action.BeginInvoke(x, null, null));
}
方法5:BackgroundWorker
(见:http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx)
要设置后台操作,请为DoWork事件添加事件处理程序。在此事件处理程序中调用耗时的操作。要启动该操作,请调用RunWorkerAsync。要接收进度更新通知,请处理ProgressChanged事件。要在操作完成时收到通知,请处理RunWorkerCompleted事件。
// using System.ComponentModel.BackgroundWorker
void Do_5()
{
var _List = GetList();
using (BackgroundWorker _Worker = new BackgroundWorker())
{
_Worker.DoWork += (s, arg) =>
{
arg.Result = arg.Argument;
DoSomething(arg.Argument);
};
_Worker.RunWorkerCompleted += (s, arg) =>
{
_List.Remove(arg.Result);
if (_List.Any())
_Worker.RunWorkerAsync(_List[0]);
};
if (_List.Any())
_Worker.RunWorkerAsync(_List[0]);
}
}
我认为明显的批评是:
但是,你如何选择? 提前感谢您的见解。
答案 0 :(得分:15)
以任意顺序拍摄:
<强> BackgroundWorker (#5) 强>
我喜欢在使用UI时使用BackgroundWorker。它的优点是在UI线程上触发进度和完成事件,这意味着当您尝试更改UI元素时,不会出现令人讨厌的异常。它还有一个很好的内置报告进度的方式。这种模式的一个缺点是,如果你的工作中有阻塞调用(如Web请求),那么在工作发生时你就会有一个线程无所事事。如果您认为自己只有少数几个,那么这可能不是问题。
<强> IAsyncResult/Begin/End (APM, #4) 强>
这是一种广泛而强大但难以使用的模型。错误处理很麻烦,因为你需要在End调用上重新捕获异常,而未捕获的异常不一定会使它回到可以处理它的任何相关代码段。这有可能永久挂起ASP.NET中的请求,或者只是在其他应用程序中神秘地消失了错误。您还必须对CompletedSynchronously
财产保持警惕。如果您没有正确跟踪和报告,程序可能会挂起并泄漏资源。另一方面,如果您在另一个APM的上下文中运行,则必须确保您调用的任何异步方法也报告此值。这意味着要进行另一次APM通话或使用Task
并将其投放到IAsyncResult
以获取其CompletedSynchronously
媒体资源。
签名中还有很多开销:如果要编写支持轮询和等待句柄的异步方法,则必须支持任意对象传递,制作自己的IAsyncResult实现(即使您只是使用回调)。顺便说一下,你应该只在这里使用回调。当您使用等待句柄或轮询IsCompleted
时,您在操作未决时浪费了一个线程。
<强> Event-based Asynchronous Pattern (EAP) 强>
一个不在你的名单上,但为了完整起见我会提到。它比APM更友好一些。有事件而不是回调,并且方法签名上挂起的垃圾更少。错误处理稍微容易一些,因为它在回调中保存并可用,而不是重新抛出。 CompletedSynchronously也不是API的一部分。
<强> Tasks (#1) 强>
任务是另一个友好的异步API。错误处理很简单:异常始终存在于回调检查中,并且没有人关心CompletedSynchronously。您可以执行依赖项,这是处理多个异步任务执行的好方法。您甚至可以在其中包装APM或EAP(您错过的一种类型)异步方法。使用任务的另一个好处是您的代码不关心如何实现操作。它可能会阻塞线程或完全异步,但消费代码并不关心这一点。您还可以使用任务轻松混合APM和EAP操作。
Parallel.For方法(#3)
这些是在任务之上的额外帮助程序。如果您的异步任务适合在循环中运行,他们可以完成一些为您创建任务并使您的代码更具可读性的工作。
ThreadPool.QueueUserWorkItem(#2)
这是一个低级实用程序,ASP.NET实际上用于所有请求。它没有像任务那样的任何内置错误处理,因此如果你想了解它,你必须抓住所有内容并将其重新管理到你的应用程序。它适用于CPU密集型工作,但您不希望对其进行任何阻塞调用,例如同步Web请求。那是因为只要它运行,它就会耗尽一个线程。
<强> async
/ await
Keywords 强>
在.NET 4.5中,这些关键字允许您编写异步代码而无需显式回调。您可以等待Task
,其下面的任何代码都会等待异步操作完成,而不会消耗线程。
答案 1 :(得分:4)
您的第一个,第三个和第四个示例隐式使用ThreadPool,因为默认情况下,任务在ThreadPool上进行调度,TPL扩展也使用ThreadPool,API只是隐藏了一些复杂性,请参阅here和{{ 3}}。 BackgroundWorkers是ComponentModel命名空间的一部分,因为它们适用于UI场景。
答案 2 :(得分:2)
Reactive extensions是另一个用于处理异步编程的库,特别是在异步事件和方法的组合方面。
它不是原生的,但它是由实验室女士开发的。它适用于.NET 3.5和.NET 4.0,基本上是.NET 4.0引入的IObservable<T>
接口上的扩展方法的集合。
他们的主站点上有很多示例和教程,我强烈建议您查看其中的一些。这种模式起初看起来有点奇怪(至少对于.NET程序员来说),但值得一提,即使它只是抓住新概念。
反应式扩展(Rx.NET)的真正优势在于您需要编写多个异步源和事件。所有操作符都是为此而设计的,并为您处理异步的丑陋部分。
主要网站:http://msdn.microsoft.com/en-us/data/gg577609
初学者指南:http://msdn.microsoft.com/en-us/data/gg577611
示例:http://rxwiki.wikidot.com/101samples
也就是说,最好的异步模式可能取决于你所处的情况。对于更简单的东西,有些更好(更简单),而对于更复杂的场景,有些更易于处理和更容易处理。我不能代表你提到的所有人。
答案 3 :(得分:-2)
最后一个是2,3的最佳选择。它具有内置的方法/属性。 其他变体几乎相同,只是不同的版本/方便的包装