C#处理列表的异步选项

时间:2011-09-06 16:11:13

标签: c# multithreading asynchronous parallel-processing

我试图更好地理解我在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]);
    }
}

我认为明显的批评是:

  1. 是否比其他表现更好?
  2. 错误处理是否优于另一个?
  3. 监控/反馈是否优于其他?
  4. 但是,如何选择? 提前感谢您的见解。

4 个答案:

答案 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的最佳选择。它具有内置的方法/属性。 其他变体几乎相同,只是不同的版本/方便的包装