C#多线程学习笔记(三)任务并行库之Parallel静态类

    在此之前我们接触了线程,线程池的一部分使用方法,同时也了解到异步编程模型APM和基于事件的异步模式EAM。但是,使用线程池并不轻松。我们需要自定义方式来获取结果,当异常发生时,也需要将异常正确地传播到初始线程中。使用APM模式组合多个异步操作也需要大量工作。为了解决这些问题,.NET Framework4.0引入了一个新的关于异步操作的API,称为任务并行库TPL。TPL被认为是线程池之上的又一个抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度的API。

    这节大量参考了《C#并行编程高级教程》上的内容。《C#并行编程高级教程》Gastopn C. Hillar【美】清华大学出版社


一    Parallel静态类

    Task类是TPL的主类,但创建并行代码并不一定要直接使用Task类的实例。有时候,最好创建并行循环或者region,这种情况下可以使用Parallel静态类。

    Parallel.Invoke()方法允许并行允许一些方法,它的特点是各方法之间没有特定的顺序,例如,当我们运行Parallel.Invoke(()=>A, ()=>B, ()=>C, ()=>D))这四个方法时,我们需要至少四个逻辑内核(硬件线程)才足以让这4个方法并发运行。而如果一个或多个内核处于繁忙状态,底层的调度逻辑可能就会延迟某些方法的执行。

    此外,Parallel.Invoke适用于加载的方法耗时相近的情况,如果这些方法时间迥异,那么就需要最长的时间才能返回控制。

    循环并行化

    Parallel还提供了一个For和一个ForEach方法来处理易并行计算任务的循环并行化工作。我们常常遇到这样的循环:它一个迭代一个迭代地运行,并对每一个数据都执行着同样的操作,这时候就可以对循环进行重构,从而并行地运行这些操作。

    书中给出的例子是计算一些MD5哈希码的工作:

    先看看原始串行执行的版本:

    

注:ConvertToHexString主要将字符数组转化为十六进制表示,因为不重要这里不给出函数体。

再看看Parallel.For的并行化版本:


    Parallel.For有多种重载,但它的参数中,fromInclusive是迭代范围的第一个数,toExclusive是迭代停止的前一个数,Body表示要被调用的委托,ParallelOptions参数可以指定并行度,这些是共性。

    在处理并行循环时,我们一定要做好充分的准备,因为并行化循环的执行顺序是无法预测的,一个循环可能会分化为多个并行的迭代,换言之,迭代2和迭代50的先后是未知且可变的。

    Parallel.ForEach提供了另一种并行处理一组数据的机制。可以利用一个范围的整数作为一组数据,然后通过一个自定义的分区器(注,书中给出的译法,即Partitioner)将这个范围转换为一组数据块。每一块数据都通过循环的方式进行处理,而这些循环是并行执行的。

    再来看看GenerateMD5Hashes使用Parallel.ForEach的版本:

我们通过Partitioner.Create()方法划分了一个[1, 800000)的迭代范围,同时为了便于观察,在每个小循环开始前打印出循环的范围和开始时间。如下图所示,在我的机器上,这个分区器划分了13个范围,默认的创建范围数是根据逻辑内核数,分区大小等因素来决定的。同时由于我的机器只有4个可用内核,因此这13个小循环并不会同时启动:

    并行化的循环和会试图根据可用的硬件资源对执行进行负载均衡,这给我们一个启示,可对生成的分区数进行人为调整,使得工作负载能够更好地均衡。例如我的机器可用逻辑内核数为4,那么可以通过Partitioner.Create(1, 800000, 200000)语句设置每个分区的大小为200000使其匹配,这时多次运行并比较可以看出,运行时间有了一定的改善:

    此外,如果现有的循环是对公开了IEnumerable接口的集合进行遍历的foreach循环,那么Parallel.ForEach也能被用来对这种循环进行重构。之前我们直接给定分区集合1~800000,这一次我们使用Enumerable.Range()方法来生成一个1~800000的整数序列,通过它来重构我们的循环。

这是ForEach的第二个版本,使用了Parallel.ForEach<>(IEnumerable<> source, Action<> body)重载:


    从并行循环中退出

    普通的串行循环可以通过break语句退出,然而在并行循环的时候,退出委托的主体方法对并行循环的执行没有任何影响,因为委托主体方法与传统的循环结构没有关系,只是在每次迭代的时候被调用。

    再回到上面给出的第二个ForEach,这次我们设置当运行时间超过3s时通知并行循环尽快停止,关注我们的ParalellLoopState局部变量:

    

    ParallelLoopState提供了从内部请求取消并行循环的可能性(外部取消请求可以用之前提到过的CancellationToken和CancellationTokenSource,后续还会提及),它有两个方法Stop()和Break()可以用于停止Parallel.For和Parallel.ForEach的执行,其区别在于:Stop()会立即退出循环,而Break()则告知并行循环在完成当前迭代之后尽快停止循环。

    直观点说,由于并行,迭代500~750已启动,而迭代250~500还没有启动,而我们设置的退出并行循环条件是大于500,那么Stop()直接退出循环,而Break()可以保证迭代250运行完毕再退出。Stop()相当于串行循环的break,而Break()相当于串行循环的continue。

    Parallel.For和Parallel.ForEach并非是没有返回结果的,其返回一个ParallelLoopResult对象,可以通过其只读属性IsCompleted和LowestBreakIteration来获知并行循环的运行情况。

    IsComplete = true,循环执行完成

    IsComplete = false && LowestBreakIteration.HasValue = false,Stop终止

    IsComplete = false && LowestBreakIteration.HasValue = true,Break终止


猜你喜欢

转载自blog.csdn.net/saasanken/article/details/79591923