初识Parallel Extensions之TPL(三)
初识Parallel Extensions之TPL(三)
前面我们谈了结构并行化的相关内容,包括For, ForEach, Do.(具体参见:初识Parallel Extensions之TPL,初识Parallel Extensions之TPL(二)),今天我们来继续谈谈任务并行化的内容,以前主要是谈如何使用TPL,由于这些在文档上基本上也都有,所以今天我们就主要集中在部分实现上,以使大家更明白其原理,所以在这里代码的例子不会太多。(转载请注明出处http://lazybee.cnblogs.com/,谢谢)
任务并行性
System.Threading.Parallel类使用For, ForEach, Do来解决一些普遍的数据和任务并行性问题。这些命令都是建立在System.Threading.Tasks.Task这个更低级别的用于灵活解决并行问题的类之上的。Task类表示一个轻量级的可以并行运行的工作单元,并且Task之间支持父/子关系,可以显式的支持成组、取消和等待等功能。所有的Task都由TaskManager(任务管理器)来进行调度运行的。如果你在创建Task时没有指定TaskManager,那么系统将自动为此Task指定TaskManager.Default来管理此Task,所有没有指定TaskManager的Task都是在这个缺省的实例中来运行的,这个缺省的TaskManager就是通过缺省的TaskManagerPolicy而实例化的一个TaskManager的实例:
public static TaskManager Default
{
get
{
return s_defaultTaskManager;
}
}
public TaskManager() : this(new TaskManagerPolicy())
{}
在这里缺省的TaskManagerPolicy就是new TaskManagerPolicy(),这个实例其实等效于下面的定义:new TaskManagerPolicy(0, Environment.ProcessorCount, 0, 0, false, false)
这就意味着缺省情况下,TaskManager将会为每个CPU内核创建一个工作线程(这样可以保证操作系统执行的线程切换的次数最少,以提高运行效率。),并且每个工作线程都有一个等待执行的Task的队列。当使用Task.Create方法创建任务时,TaskManager只是将任务放入Task的队列中,等待工作线程来执行;当Task运行完成之后,将弹出队列。如果某个工作线程的Task队列为空时(任务都已执行完),这个工作线程将会去寻找其他工作线程的Task队列是否有任务等待执行,如果存在就会将其“窃取”过来自己执行以达到动态分配工作的目的,这就是TPL所实现的工作窃取技术。
以前使用线程池手动实现工作任务的并行化时,开发人员通常是静态划分工作单元,由于这样可能会导致工作分配不平均,导致部分线程工作已经完成,而另外部分线程却还在执行,完成工作的线程只能静静的等待,有点“坐山观虎斗”情景。当然有人会说了,我把任务分平均了不就不会存在这个问题了吗?其实也不尽然,有可能页面错误或操作系统上并行运行的其他线程,也可能导致这种情况发生。 TPL使用工作窃取技术使任务动态地分配到工作线程中,可以有效的避免“坐山观虎斗”的状况,其优势在于工作线程之间几乎没有同步,因为工作队列是经过分配的,而且大多数操作对工作线程来说都是局部的,这对可伸缩性而言至关重要。此外,工作窃取具有已证实的良好缓存区域和工作分配属性。例如,如果工作负荷不平均,某个工作线程可能需要很长时间来完成某个特定任务,但是其他工作线程现在将从它的队列中窃取工作,保证所有处理器都处于忙碌状态。动态工作分配在典型的应用程序中至关重要,原因是很难预期某个任务需要多长时间才能完成。对于桌面系统(其中多个不同进程共享多个处理器,而且我们无法预期工作线程将得到的时间片)而言,情况尤其如此。
针对并行程序一个令开发人员非常头疼的就是调试,在这里我们可以将TaskManager的理想并发线程数设成1,也就是说我们可以创建自定义的TaskManager来使得程序顺序执行,这样就可以减轻我们调试的痛苦:
public TaskManager(new TaskManagerPolicy(0,1,0,0,false,false))
我们只需要在创建任务时使用带TaskManager参数的重载方法,即可达到目的。然后在调试完成之后再将理想并发线程数改成Environment.ProcessorCount,或者使用缺省的TaskManager即可。