一.基于任务的程序设计
共享内存多核OS和分布式内存OS
共享内存多核OS-一个微处理器由多个内核组成,且每个内核共享一段私有内存;
分布式内存OS-- 由多个微处理器组成,每个微处理器可以有自己的私有内存,微处理器可以位于不同的计算机上,每个计算机可以有不同的通信信道消息传递接口(MPI):运行在分布式内存计算机系统上的并行应用程序所使用的最流行的通信协议;
并行程序设计和多核程序设计
并行程序设计是指同一时刻运行多条指令,编写的代码能够充分利用底层硬件提供的并行执行能力;
多核程序设计能够充分利用多个执行内核并行运行多个指令;
硬件线程和软件线程
物理内核(physical core)--真正独立的处理单元,多个物理内核使多条指令能够同时并行的运行。每一个物理内核可提供多个硬件线程(亦称逻辑内核或逻辑处理器);
对称多线程(simultaneous multithreading,SMT):使用超线程计技术(HT)使微处理器在每个物理内核上提供多份架构状态,从而获得了 物理内核数X架构状态数 个硬件线程;
软件线程:每一个软件线程与其父进程分享一个私有的唯一的内存空间,但每一个软件线程有自己的栈、寄存器和私有局部存储区域;可以将硬件线程比作泳道,软件线程比作游泳者;
负载均衡:将软件线程的任务分发在多个硬件线程的操作,通过负载均衡,工作负载可以公平地分配在硬件线程之间。实现负载均衡取决于应用程序的并行程度、工作负载、软件线程数、可用的硬件线程以及负载均衡策略;
Amdahl法则
预测多处理器系统的最大理论性能提升(加速比)
公式:最大加速比=1/((1-P)+(p/N))
P指能够完成并行运行的代码比例
N指可用的计算单元数(处理器或物理内核数)
Gustafson法则
通过问题的大小来测量在固定时间内的可以执行的工作量;
总工作量(单元数)=S+(N*P);
S表示一次顺序执行完成的工作单元数;
P表示每一部分能够完全并行执行的工作单元数;
N表示可用的执行单元数(处理器数或物理内核数)
重量级并发模型和轻量级并发模型
重量级并发模型(多线程编程模型):编写复杂的多线程代码;将算法分解为多个线程、协调各个代码单元、在代码单元之间共享信息以及收集运算结果等任务;并且多线程模型过于复杂,难以应对多核革命;由于框架层次缺乏对多线程范围的支持,多线程需要做大量处理,这会导致代码复杂难以理解;
轻量级并发模型:减少了在不同逻辑内核上创建和执行代码所需要的总开销,并不只是关注不同逻辑内核之间的作业调度,还在框架级别添加对多线程访问的支持;.net framework 4.0实现了该模型;
交错并发,并发和并行
交错并发(interleaved concurrency):一次执行一个线程的指令,两个线程的指令交错执行
并发(concurrency) :两个线程的指令同时执行
并行化要求:对需要完成的工作进行划分、并发的运行处理划分的部分、并且能够整合运行结果;对一个问题进行并行化就会产生并发性;
多核并行程序设计原则
按照并行的方式思考;
使用抽象编程(TPL任务并行库);
按照任务(事情)编程,而不是按照线程(CPU内核)线程--通过TPL,可以编写代码实现基于任务的的设计,而不用关注底层的线程;
设计的时候要考虑关闭并发的情形;
避免使用锁--TPL在很多复杂的情况下使得避免使用重量级的锁更加简单,TPL还提供了新的轻量级的同步机制;
利用为帮助并发而设计的工具和库;
使用可扩展的内存分配器;
设计的时候要考虑增长的工作负载而扩展;
CoreInfo工具 --查看处理器信息程序
二.命令式数据并行
TPL支持数据并行(对每一份数据执行相同的操作)、任务并行(并发的运行不同的操作)和流水线(任务并行和数据并行的结合体);
Parallel.Invoke ----对给定的独立任务提供潜在的并行执行;
需要传入一个要并行执行的Action委托的参数数组;方法没有特定的执行顺序,只有在所有方法都执行完之后才会返回;
优势:并行运行很多方法的简单方式,不用考虑任务和线程的问题;
循环并行化----Parallel.For和ForEach,不支持浮点数和步进。无法保证迭代执行的顺序;
Parallel.For ----为固定数目的独立For循环迭代提供了负载均衡的潜在的并行执行;负载均衡的执行会尝试将工作分发在不同任务中,这样所有的任务在大部分时间内都可以保持繁忙。负载均衡总是试图减少任务的闲置时间。
Parallel.ForEach----为固定数目的独立For循环迭代提供了负载均衡的潜在的并行执行;支持自定义分区器,让你可以完全掌控数据并发;提供了20种重载方法,source参数表示分区器;
利用一个范围整数作为一组数据,通过一个分区器,把数据分成一组数据块。每一块的数据都通过循环的方式处理,而这些循环都是并行的。
在并行循环中使用分区:Partitioner.Create(1, NUM_AES_KEYS + 1);
根据内核数目优化分区:Environment.ProcessorCount 获取逻辑内核的个数;
Partitioner.Create(1,NUM_AES_KEYS, ((int)(NUM_AES_KEYS / Environment.ProcessorCount) + 1))
使用IEnumerable接口的数据源作为分区器;
从并行循环中退出:
在参数中使用ParallelLoopState,就可以使用loopState.Break()或者loopState.Stop()进行退出。其中的差别在于,假设调用Break的时候正在处理迭代100,那么可以保证小于100的迭代都被执行,而Stop不保证这个而是告诉并行循环应尽快停止执行。ParallelLoopResult作为返回值,可以知道是否是正常完成或者被Break的;
捕获并行循环的异常:
try
{
loopResult =Parallel.ForEach(inputData,
(int number, ParallelLoopStateloopState) =>
{ throw new Exception(); });
}
catch (AggregateException ex)
{
foreach (Exception innerEx in ex.InnerExceptions)
{
Debug.WriteLine(innerEx.ToString());
}
}
指定并行度:ParallelOption用于修改最大并行度。
var parallelOptions = new ParallelOptions();
parallelOptions.MaxDegreeOfParallelism = maxDegree;
二.命令式任务并行
任务是使用底层线程(软件线程)运行的,但任务和线程不是一对一的关系;CLR会创建必要的线程来支持任务执行的需求;默认的任务调度器依赖于底层的线程池引擎;在创建一个任务时,调度器会使用工作窃取队列(work-stealing queue)找到一个合适的线程,然后将任务加入队列;一个Task表示一个异步操作;
TaskStatus状态:初始状态——>TaskStatus.Running状态————>最终状态(如果Task实例有关联的子任务,Task将转变为TaskStatus.WaitingForChildrenToComplete,当子任务都完成后才会进入最终状态)
Task.WaitAll方法采取的是同步执行方式,其重载方法可以指定要等待的毫秒数,返回一个bool值,表明任务是否在指定时间内完成;
取消标记(cancellation token):中断任务的执行