Parallel

并发和并行

  • Parallel:并行,同时做多件事情,关于执行、实现
  • Concurrent:并发,能够处理多件事情,关于结构、逻辑
  • 并发问题可以使用并行方式解决,也可以串行解决

    • 100并发任务同时运行在4核CPU上,最多可能有4个并发任务 并行处理,其余只能是串行处理
  • 并行角度:硬件技术的物理限制瓶颈

    • 单计算核心能力不足,所以需要多核并行运算
    • 进程、线程可认为是实现并行的基本逻辑实体
  • 并发角度:程序执行的逻辑重用需求

    • 程序要求重用一组逻辑,所以需要将一组指令集打包,重复 调用该组指令集
    • 子程序、协程可认为是方便重用的基本逻辑实体,因此更 应是语言内建机制
      • 子程序:无状态保存,同样重入得到同样结果
      • 协程:有保存状态,重入会改变协程状态,结果可能 不同
  • 线程作为任务执行的实体,可以认为是子程序、协程的具体执行

    • 内核线程作为可以独立执行的实体,逻辑上更会被设计为 完成独立任务,即没有保存状态需求,因此多是子程序的 具体执行
    • 用户线程则用程序负责调度,二者执行实例均可
    • 某种意义上线程、子程序是对应的执行实体、逻辑实体

子程序、协程

  • 子程序可以看作时协程的特例
    • 只有一个状态,每次进入时局部状态重置
    • 唯一入口点
  • 协程可视为子程序的组成
    • 维护自身状态,所以逻辑上不独立 ,应该是作为被 调用对象
    • 对于每次返回部分结果值的协程(也称生成器迭代器), 可以直接视为类似链表之类的数据结构 (在某些语言中可以所有数据都是类,从这个角度这也都是 统一的)
子程序 协程
生命周期 后进先出 完全取决于需要
入口点 起始处 起始处、yield返回出口点
返回值 调用结束后返回全部 可每次yield返回部分值
  • 现代指令集通常提供对调用栈的指令支持,便于实现可递归 调用的子程序,在提供续体的语言环境(如Scheme),恰好 可用此抽象状态表示实现协程

Subroutine/Procedure/Function/Routine/Method

子程序:打包为整体、用于执行特定任务的指令集序列

  • 子程序是依赖可重入能力的弱化版本

    • 一旦唤醒,于起始点开始执行
    • 一旦退出,子程序结束
    • 子程序实例只返回一次,两次激活间不保存状态
  • 子程序中局部变量在每次调用/重入函数时都是相同的

    • 相同输入得到相同输出
  • procedure:过程,有时特指无返回值、仅有副作用

线程安全

线程安全:子程序在多线程环境调用时,能够正确处理多个线程之间 的共享变量,使程序功能能正确完成

  • 线程安全函数应该为每个调用其的线程分配专门空间,存储需要 单独保存的状态

  • Atomicity:原子性,操作不会被线程调度机制打断,一旦 开始就会运行到结束,中间不会有任何线程切换

    • 可以通过locksynchronized确保原子性
  • Visibility:可见性,某线程修改变量值后,其他线程能够 立刻感知
    • 一般可以通过volatile保证可见性,强制要求被修改值 从寄存器同步至主存
    • locksynchronized也可以通过限制其他线程访问变量 的方式保证可见性
  • Ordering:有序性/一致性,程序按照代码顺序执行
    • 可以通过volatile保证一定的有序性
    • 也可通过locksynchronized提供单线程执行环境保证 有序性

Instruction Reorder

指令重排:编译器对无相互依赖的指令重新排序执行

  • as-if-serial语义:指令可以为优化而重排序,但是必须保证 最终执行结果不变

    • 规则:重排序过程不破坏数据依赖关系
    • 只能保证单线程执行结果有效,但不保证多线程并发执行 的正确性

    instruction_reorder_asif

  • happens-before原则:保证前后两个操作间不会被重排序,

    • 程序次序规则:线程中每个操作happens-before该线程中 任意后续操作
    • 锁定规则:锁的解锁happens-before加锁
    • volatile变量规则:volatile变量写操作happens-before 其读操作
    • 传递规则:若A happens-before B、B happens-before C,则A happens-before C
    • 线程启动规则:线程对象启动happens-before线程中每个 动作
    • 线程中断规则:线程中断方法的调用happens-before被 中断线程代码检测到的中断事件的发生
    • 线程终结规则:线程中所有操作happens-before线程的 终止检测
    • 对象终结规则:对象的初始化happens-beforefinal 方法的开始
  • happens-before原则被JVM用于规定(跨线程)操作之间偏序 关系,若操作之间的关系可以由此原则退出,则两个操作有序

Reentrant

  • A computer program or routine is described as reentrant if it can be safely executed concorrently; that is, the routine can be re-entered while it is already running

可重入函数:对于相同(合法)的函数参数,多次重复调用(包括 执行过程中被中断再重入)结果总是可预期的

  • 可重入需要满足条件

    • 不在函数内部使用静态或全局数据,所有数据都由函数 调用者提供
      • 全局变量区
      • 中断向量表
    • 使用本地数据,或制作全局数据的本地拷贝保护全局数据
    • 不返回静态或全局数据
    • 不调用不可重入函数
  • 不可重入后果主要体现在信号处理函数这样需要重入情况中, 若在信号处理函数中使用了不可重入函数,则可能导致程序错误

  • 可重入函数总是线程安全的,反之不一定成立

    • 线程安全可以通过“并发不冲突”实现
    • 可重入则要求“并行不冲突”

Coroutine

协程:为非抢占式多任务产生子程序的程序组件,允许执行过程 中挂起、恢复

  • 挂起、恢复:协程可以通过yield(让步)调用其他协程暂时 退出,之后可在退出位置恢复执行

    • 从协程角度看,这是调用其他协程而不是退出
    • 但实际是各协程之间是对称的,而不像子程序调用 中主调-被调关系
    • 这即暗含
      • 协程可包含多个入口点
      • 允许在不同入口点暂停、开始执行程序
  • 局部状态维护:协程实例保持上次退出时状态

    • 则协程被唤醒时状态可能不同
    • 可能同时有多个给定协程实例
  • 协程将原在子程序外、输入状态管理工作交由自身逻辑维护
  • 原生不支持协程的语言也可以使用循环等构建
  • 经典状态机、对象已经具有协程特性

用途

  • 协程可以简化异步代码的实现,使得需要使用异步+回调的代码 可以使用看似同步方式写出

    • 协程本身只涉及状态保存、过程重入,和并发/异步无关系
    • 但协程本身蕴含的状态保存使得状态切换几乎无成本,适合 高并发任务
  • 协程在线程中调度完全由用户控制,可以视为用户态轻量级线程

    • 避免陷入无效内核级别上下文切换造成的性能损失
    • 较线程在IO密集任务上性能上更好

进程、线程

  • 这里仅讨论理论上的异同,不考虑各平台具体实现
  • 进程:具有独立功能的程序关于某数据集合的一次运行活动
  • 线程/子程序:进程内某特定任务的执行活动
  • 协程:推广协作式多任务的子程序
Process Thread Coroutine
调度、创建、切换、维护 内核(系统) 内核、自身 自身
切换开销、速度 大、慢 小、快
易用性 需要考虑进程退出、僵尸进程 只需要管理进程即可 无需管理
资源共享 独立 同进程内线程共享资源 除局部变量外均共享
通信 IPC较复杂:环境变量、文件、系统端口 较简单:共享内存 结果调用
移植性
健壮性 好,进程死亡不影响其他进程 差,线程死亡会导致进程(及线程)死亡
  • 进程、线程、协程可以看作是控制权逐渐从系统已经移交到自身 的过程
  • 这里的协程强调其在实现方面的资源、调度特点,其与子程序间 功能差异参加cs_program/parallel/implementation
  • 通信:参见cs_program/parallel/#todo
  • 移植性:基于进程分支多进程和windows模型有很大冲突,往往 不能在windows平台上使用

Process

进程:具有独立功能的程序关于某数据集合的一次运行活动

  • 进程是处于运行期的程序和相关资源的总称

    • 程序:代码、指令
    • 运行:对CPU的占用,逐渐发展为线程,标识进程中指令的执行
    • 资源:执行上下文,由进程内线程共享
  • 从系统调度方面看

    • 进程是系统进行资源分配、调度的独立单位
    • 在系统中有进程控制块(进程描述符)描述进程相关信息, 系统通过此控制块控制系统相关行为
  • 从资源分配方面看

    • 有独立的存储空间(虚拟寻址空间)
      • 独享的用户空间
      • 进程专用的“共享内核空间”
    • 可执行的程序代码
  • 线程可能对系统是可感知的,则进程不定是资源分配的基本 单位
  • Linux线程实现即类似进程,但不包含独立存储空间

调度角度

  • 内核跟踪进程运行所需的状态信息(上下文)

    • 主存、虚拟内存内容
    • 寄存器文件值
    • 文件句柄
  • 调度:分配CPU执行进程

    • 内核决定CPU控制权在进程间的转移
  • 上下文切换:进程状态的记录、恢复、切换

    • 保存当前进程上下文、恢复新进程上下文
    • 通过处理器在进程间切换,实现单个CPU“看似”并发执行 多个进程
    • 上下文进程间切换开销比较大,但相对比较稳定安全

资源角度

  • 独立内存空间/虚拟地址空间:每个进程“独占的”使用 内存、看到一致的存储器

process_virtual_address_space_structure

  • 用户空间

    • 程序代码、数据:对所有进程,代码从同一固定位置开始, 直接按照可执行目标文件的内容初始化
    • (运行时)堆:可在运行时动态扩展、收缩
    • 共享库:共享库代码和数据,如:C标准库、数学库
    • 栈:用于实现函数调用,运行时动态扩展、收缩,位于虚拟 地址空间顶部
  • 内核空间

    • 内核虚拟存储器:内核总是驻留在内存中,为其保留地址 空间顶部
    • 不允许程序读写区域内容或直接调用内核代码定义的函数

Thread

线程:进程执行实体,进程中包含指令的执行活动

调度角度

  • 线程是CPU调度、分派的基本单位,有时被称为轻量级进程 (在Linux系统中也是按照轻量级进程实现)
  • 线程引入逻辑

    • 进程内部可能存在多个不同task,task需要共享进程数据
    • 同时task操作的数据具有独立性,多个task不需要按照时序 执行
    • task间需根据不同策略进行调度,因此产生了线程概念, 并被引入作为内核调度基本单位
  • 线程:比进程更小的、能独立运行的基本单位

    • 线程能/是“独立运行”,但不一定能被内核感知到,也一定由 内核调度
    • 只能说线程是针对某个task的执行活动

资源角度

thread_virtual_address_space_structure

  • 线程运行在进程的上下文中,共享同样代码和全局数据

    • 进程代码段、公有数据
    • 进程打开的文件描述符、信号的处理器
    • 进程当前目录
    • 进程用户ID、进程组ID
  • 线程还独享某些个性以实现并发性

    • 线程ID:进程中唯一标识
    • 寄存器组值:线程间并发运行,线程有不同运行线索, 切换时需要保存当前线程的寄存器集合状态
    • 线程堆栈:独立函数堆栈保证线程内函数调用可以正常 执行,不受其他线程影响
    • 错误返回码:线程的系统调用错误可能未及时处理,独立 错误返回码避免其被其他线程修改
    • 线程的信号屏蔽码:线程感兴趣的信号不同,因此信号 屏蔽码应由自己管理
    • 线程优先级:线程需要像进程被调度,需要有相应的优先级

线程实现理论

User-Level Thread

用户级线程:由用户程序自身负责支持、调度

  • 特点

    • 相当于实现自己的线程调度内核,实现线程数据结构、 创建、销毁、调度维护
    • 线程运行在内核(可感知的)进程内
  • 优点

    • 即使系统不支持线程,也可通过库函数支持在系统中实现 真实的多线程
    • 线程只在用户态,减少内核态到用户态切换开销
  • 缺点:线程对系统透明,对系统每个进程只有一个线程, 系统直接调用进程

    • 当线程进行系统调用而阻塞时,系统会阻塞整个进程
    • 用户空间没有时钟中断机制,若线程长时间不释放CPU, 会导致阻塞其他线程

Kernel-Level Thread

内核级线程:系统内核支持的线程,通过内核完成线程切换

  • 优点/特点:系统负责线程的创建、销毁、调度、维护

    • 内核通过操纵调度器对线程进行调度,并负责将线程 的任务映射到各个处理器上
    • 程序可以直接使用系统调用已实现线程,无需实现线程 调度、对CPU资源抢占使用
  • 缺点:内核线程需要内核支持

    • 创建、销毁、调度、维护往往都需要系统调用,代价较高
    • 需要消耗内核资源,不能大量创建

线程调度模型

  • X对Y是指X task对应Y内核调度单位

N对1模型

thread_model_n_versus_1

N对1模型:线程库有实现用户级线程,内核未实现内核级线程

  • 系统只负责调用进程
  • 线程对系统透明,由进程自身负责调用
  • 此模型中内核没有实现内核级线程,所以内核调度单位就是进程 S

    1对1模型

thread_model_1_versus_1

1对1模型:线程库未实现用户级线程,内核实现有内核线程

  • 程序(逻辑)层面

    • 创建执行独立task的线程
    • 程序创建的线程直接由内核负责调度
  • 内核(实现)层面

    • 每次创建线程都是调用系统调用创建内核级线程
    • 可视为每个“用户创建的线程”同内核级线程绑定,二者一一 对应
  • 此模型中内核调度单位就是内核级线程
  • 此模型中不存在上述定义的用户级线程,图中Thread实际上 应该就是Kernel Thread,仅拆分出来表示是程序创建的线程

M对N模型

thread_model_m_versus_n

M对N模型:线程库实现有用户级线程,系统也实现有内核级线程

  • 程序(逻辑)层面

    • 创建执行独立task的用户级线程
    • 创建可独立被内核调度的内核级线程
    • 将若干和用户线程同内核线程相关联,即task组总体由内核 负责调度,task组内task由程序自身负责调度
  • 内核(实现)层面

    • 调用系统调用创建内核级线程
    • 内核线程执行指令中包含用户线程创建、调度指令
  • 优点

    • 用户级线程创建、切换、析构等均在用户空间中,依然 廉价,支持大规模用户线程并发
    • 内核级线程作为用户线程在内核中调度、执行的桥梁, 降低整个进程被完全阻塞的风险
  • 此模型中内核调度单位为内核级线程、task单位为用户线程, 二者比例不定
  • 有的说法里面会使用Light Weighted Process表示内核级线程

通用调度算法

  • 耗时相差不大的task队列总是比较好处理,难以调度的task是

    • 耗时相差大
    • IO任务、计算任务混合
  • 内核调度除考虑任务(线程)外,还会考虑进程因素

    • Gang Scheduling:尽量将同进程中线程同时调度,而非 随机从多个进程中挑选CPU数量线程调度
    • Space Sharing:将CPU划分,各进程仅允许占用部分CPU 执行并发
  • 从任务角度,调度需要考虑

    • Responsiveness
    • Schedule Overload
    • Starvation-Freedom:饥饿避免
    • Fairness

First-In-First-Out

先进先出:按task在队列中的顺序依次调用,执行完task再执行下个 task,仅在task结束后才会切换task

  • 优点

    • 最少task切换开销
    • 最大吞吐量(总处理效率)
    • 朴实公平
  • 缺点

    • 平均相应时间高

Shortest task First/Shortest Remained Time task

最短耗时task优先:有限调度耗时短task

  • 优点

    • 平均相应时间短:长耗时task不断推移,必然统计出较短 平均响应时间
  • 缺点

    • 不公平,长耗时task难被调度,容易饥饿
    • 频繁task切换,调度额外开销大

Round Robin

时间片轮转:给队列中每个task时间片,时间片结束之后task移至 队列末尾,切换到执行下个task

  • 优点

    • 每个task可以得到公平调度
    • 耗时短task即使在耗时长task之后也可以较快得到执行
  • 缺点

    • task切换引起的调度开销大,需要多次切换task上下文
    • 时间片不好设置
      • 时间片足够小则退化为SFJ
      • 时间片足够大则退化为FIFO
    • 需要知道task(剩余)执行时间

(Weighted) Max-Min Fairness

(带权重的)最大最小公平:资源按照需求递增的顺序分配,不存在 需求得到资源超过自身需求,未得到满足得到需求等价分享资源

  • 具体方案
    • 每轮开始将资源按照权重分配
    • 若需求大于被分配资源则推迟执行,进入下轮
    • 若需求小于被分配资源则执行,并将多余资源继续按照权重 分配给无法执行资源

Multi-level Feedback Queue

多级反馈队列:监控task处理耗时,若task未用尽分配资源则提高 优先级,否则降低其优先级

  • 具体方案

    • task、分片时长具有相对应的不同优先级
      • 分片时长越长优先级越低
      • 高级优先级task可以抢占低优先级task
      • 新task位于高优先级task
    • 同一优先级task使用Round Robin (事实上仅有最低优先级task使用Round Robin算法, 其他优先级都是FIFO)
      • 时间片用完后task结束则正常退出系统,否则优先级 下滑一等级
      • 若是主动让出CPU(IO等),则停留在当前优先级或 提升
  • 多核场景

    • 应该为每个CPU分配单独MFQ,同时采用 Affinity Scheduling保证task尽量同一CPU核上执行, 避免CPU cache频繁失效
    • 若共用MFQ,则容易出现
      • 随CPU增长,对MFQ锁争抢严重
      • task每次执行位于的CPU不同,CPU Cache需要在不同 CPU间转移

同步问题

生产者-消费者问题/缓存绑定问题

  • 生产者生成数据放入缓存,消费者从缓存获取、移除、消费数据 ,问题核心在于保证不让生产者在缓存已满时放入数据、不让 消费者在缓存为空时读取数据
  • 若缓存满:生产者者停止工作
  • 若缓存空:消费者停止消费
  • 消费者从缓存中取走数据,通知生产者工作
  • 生产者向缓存中放入数据,通知消费者消费
  • 不完善的解决方案会造成死锁:生产者、消费者均等待对方唤醒

信号量解决方案

  • mutex信号量:互斥信号量,确保只有一个生产者、消费者 操作缓存区,单一消费者、生产者可省略此信号量
  • fill_count信号量:已使用缓存区数量
  • empty_count信号量:空闲缓存区数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
semaphore mutex = 1
semaphore fill_count = 0
semaphore empty_count = BUFFER_SIZE

producer():
while true:
item = produce_item()
down(empty_count)
down(mutex)
put_item_into_buffer(item)
up(mutex)
up(fillcount)

consumer():
while true:
down(fill_count)
down(mutex)
item = remove_item_from_buffer()
up(mutex)
up(empty_count)
consume_item(item)

状态监控解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
item_count = 0
condition full
condition empty

add(item):
while item_count == BUFFER_SIZE:
wait(full)

item_count = item_count + 1
put_item_into_buffer(item)

if item_count == 1:
notify(empty)

remove():
while item_count == 0:
wait(empty)

item_count = item_count - 1
item = remove_item_from_buffer()

if item_count == BUFFER_SIZE - 1:
notify(full)

produer():
while true:
item = produce_item()
add(item)

consumer():
while true:
item = remove()
consume_item(item)
  • 互斥信号没有保护关键区,监控方法更好

其他

  • 协调生产者、消费者的关闭

    • 可在在队列中放置特殊的值,消费者读到时终止执行,结束 消费者线程
    • 有多个消费者时,消费者读取特殊值之后可将特殊值放回 队列中继续传递,直至关闭所有消费者

哲学家就餐问题

  • 哲学家围坐在圆桌旁,只能吃饭或者思考,每两个哲学家 之间只有一根筷子,只有同时拿到左右两根筷子才能正常吃饭
  • 实际计算机问题中,筷子视为共享资源

服务生

  • 引入服务生判断资源是否能被获取
  • 引入服务生,哲学家必须经过其允许才能拿起筷子

  • 服务生知道有哪些筷子正在被使用,能够判断是否会死锁

资源分级

  • 为资源(筷子)分配偏序关系
    • 约定所有资源都按此偏序获取、相反顺序释放
    • 且保证不会有无关资源同时被同一工作获取
  • 哲学家只能拿左右侧筷子:不会有无关资源被同一工作获取

  • 将筷子按顺序编号:资源分配偏序关系

    • 哲学家只能先拿左右筷子中编号较小者
    • 哲学家需要先放下筷子中编号较大者
  • 最实用的解法:为锁指定常量分级,强制获取顺序的顺序
  • 策略不总是实用的,尤其是所需资源列表事先不知道,可能需要 先释放已获取资源、获取低编号资源、重新获取资源,效率不高

Chandy/Misra解法

  • 标记资源,保留未使用资源、交出已使用资源,初始所以资源 已使用
  • 每根筷子分为干净、脏,最初所有筷子都脏

  • 对每对竞争同一筷子的哲学家,新拿筷子给编号较低者

  • 当哲学家需要某筷子时

    • 向其竞争对手发送请求
    • 拥有筷子的哲学家收到请求
      • 若筷子干净则保留
      • 否则擦干净交出
  • 哲学家吃完东西后,筷子变脏

    • 若有哲学家之前请求过该筷子,擦干净交出
  • 有很大并行性,适合任意大问题

读者-写者问题

  • 多线程同时访问共享内存地址,线程写入时其他线程不能读取、 写入,多个线程可以同时读取
  • 一般使用readers-writer lock解决问题

读者优先

  • 若共享内存被读取,其他读者可以立即、同时读取
  • 若一直有读者开始读取,则写者会一直被插队、无法修改

写者优先

  • 如果写者在排队,应该尽快写入共享内存
  • 若一直有写者准备写入,则读者会一直被插队、无法读取

限定时间

  • 共享内存区的锁定权要在限定时间内结束
  • 能避免读者、写者一直排队

熟睡的理发师问题

  • 理发店只有一名理发师、一张理发时坐的椅子、若干普通椅子 供顾客等待
    • 没有顾客时理发师在理发椅子上睡觉,顾客到达后离开、 或者叫醒理发师
    • 有顾客时,理发师为别人立法,顾客达到后若有空闲普通 椅子则坐下休息、否则离开
    • 理完发后,任选顾客开始理发
  • 理发师等待顾客、顾客等待理发师,造成死锁
  • 有顾客不按顺序等待,让某些顾客永远不能理发

3信标解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
semaphore customer = 0
semaphore barber = 0
semaphore mutex = 1
empty_chairs = BUFFER_SIZE

barber():
while true:
if empty_chairs == BUFFER_SIZE:
sleep()

down(mutex)
item = get_customer_from_chairs()
empty_chairs += 1
up(mutex)

down(barber)
cut_hair(item)
up(barber)

customer():
while true:
down(mutex)
if empty_chairs > 0:
empty_chairs -= 1

else:
wait() or leave()

up(mutex)

三个烟鬼问题

  • 香烟需要:烟草、卷烟纸、火柴,三个烟鬼分别有无限各一种, 不吸烟协调人会随机安排两个烟鬼各拿出一份材料放在桌上, 另外一个烟鬼拿到材料卷烟、抽
    • 桌上空了后,协调人就随机要求烟鬼拿出材料
    • 烟鬼只会在抽完手中烟后才会卷另一只
    • 若烟草、卷烟纸在桌上,有火柴的烟鬼在吸烟,直到该烟鬼 吸完烟拿走桌上材料才会继续
  • 问题模拟程序中4种角色,展示信标方法作用有限

通信、锁

函数通信

  • 简单通信方式:允许进程向其他进程发送简单信息

    • 命令行参数
    • 信号:受控于操作系统的非同步事件机制
    • shell环境变量
    • 程序退出状态码
    • 简单文件
  • 匿名管道:允许共享文件描述符的线程、进程传递数据

    • 仅在进程内部存在,通常和进程分支合用作为父进程、 子进程之间的通信手段、线程之间通信

    • 依赖于类Unix下进程分支模型,可移植性差

  • 具名管道FIFO:映射到系统的文件系统,允许不相关程序 进行交流

    • 是真正的外部文件,通过标准文件接口实现
    • 可以独立启动,局限于本地进程通信时可以替代套接字
    • 相较于普通外部文件,操作系统同步化FIFO访问,使之 适合IPC
  • 套接字socket:映射到系统级别端口号

    • 允许远程联网机器程序之间交流
    • 可移植性较好,几乎所有平台都支持
  • 共享内存:消息保存在函数外部,不随着函数栈清除消失

    • 全局变量
    • 类中变量

  • 线程/进程调度本质上是不确定的

  • 并行编程中错误使用锁机制可能会导致随机数据损坏、其他 异常行为,即竞争条件

  • 最好在临界区(对临界资源进行操作的部分代码)使用锁

死锁、预防

  • 线程/进程同时获取多个锁

避免、预防方案

  • 尽可能保证每个线程/进程只能同时保持一个锁

  • 为程序中每个锁分配唯一ID,然后只允许按照升序规则使用 多个锁

    • 单纯按照对象ID递增顺序加锁不会产生循环依赖,而循环 依赖时死锁必要,从而避免进入死锁状态
    • 可以数学上证明,这样能保证程序不会进入死锁状态

检测、恢复方案

  • 引入看门狗计数器:
    • 线程正常运行时每隔一段时间重置计数器
    • 没有发生死锁时正常运行
    • 一旦发生死锁,无法重置计数器导致计数器超时,程序通过 重启恢复自身状态

并行计算简介

并行计算模型

PRAM

Parallel Random Access Machine:随机存取并行机器模型,也称 共享存储的SIMD模型,从串行的RAM模型直接发展起来

假定

  • 容量无限大的共享存储器

  • 有限个或无限个功能相同的处理器,具有简单的算术运算和逻辑 判断功能

  • 任何时刻各处理器都可以通过共享存储单元相互交互数据

分类

根据处理器对共享存储单元同时读、同时写的限制,PRAM模型可以 分为下面几种

  • PRAM-EREW:Exclusive-Read and Exclusive-Write,不允许同时 读、写

  • PRAM-CREW:Concurrent-Read and Exclusive-Write,允许同时 读、不允许同时写

  • PRAM-CRCW:Concurrent-Read and Concurrent-Write,允许 同时读和同时写,允许同时写是不现实的,进一步约定

    • CPRAM-CRCW:Common PRAM-CRCN,只允许所有的处理器同时 写相同的数

    • PPRAM-CRCW:Priority PRAM-CRCN,只允许最优先的处理器 先写

    • APRAM-CRCW:Aribitrary PRAM-CRCN,允许任意处理器 自由写

    • SPRAM-CRCW:Sum PRAM-CRCN,往存储器中写的实际内容是 所有处理器写的数的和

优点

  • 适合于并行算法的表达、分析和比较

  • 使用简单,很多关于并行计算机的底层细节,比如处理器间通信 、存储系统管理和进程同步都被隐含在模型中

  • 易于设计算法和稍加修改便可以运行在不同的并行计算机系统上

缺点

  • 模型中使用了全局、单一共享存储器、局存容量较小

    • 不足以描述分布主存多处理机的性能瓶颈
    • 共享单一存储器的假定,不适合分布存储结构的MIMD机器
  • PRAM模型是同步的

    • 意味着所有的指令都按照锁步的方式操作
    • 耗时长、不能反映现实中很多系统的异步性;
  • 假设不现实

    • 模型假设每个处理器可在单位时间访问共享存储器的任一 单元,要求处理机间通信无延迟、无限带宽和无开销,忽略 资源竞争和有限带宽
    • 假设处理机有限或无限,对并行任务的增大无开销

BSP模型

异步MIMD-DM(Distributed Memory)模型

  • BSP模型支持消息传递系统,块内异步并行,块间显式同步

  • 模型基于一个master协调,所有worker同步(lock-step)执行, 数据从输入的队列中读取

模型描述

模型可以用 p/s/g/i 4个参数进行描述

  • p:处理器的数目(带有存储器)。

  • s:处理器的计算速度。

  • g:选路器吞吐率

    • 定义为:time_steps / packet
    • time_steps:每秒本地完成的局部计算数目
    • packet:通信网络每秒传送的数据量
  • i:全局同步时间开销,Barrier synchronization time

同步和通信的开销都规格化为处理器的指定条数,p台处理器同时 传送h个字节信息,则gh就是通信的开销

模型结构

BSP程序同时具有水平和垂直两个方面的结构

  • 垂直上:BSP程序由一系列串行的超步(superstep)组成

    super_step_line

  • 从水平上看:在一个超步中,所有的进程并行执行局部计算

  • 超步可分为三个阶段

    super_step

    • 本地计算阶段:每个处理器只对存储本地内存中的数据进行 本地计算
    • 全局通信阶段:对任何非本地数据进行操作
    • 栅栏同步阶段:等待所有通信行为的结束

特点

  • 模型将计算划分为一个一个的超步(superstep),有效避免死锁。

  • 处理器和路由器分开,强调了计算任务和通信任务的分开,且 路由器仅仅完成点到点的消息传递,不提供组合、复制和广播等 功能,掩盖具体的互连网络拓扑、简化了通信协议

  • 一般分布存储的MIMD模型的可编程性比较差,但BSP模型中,若 计算和通信可以合适的平衡(例如g=1),则它在可编程方面 呈现出主要的优点。

  • 采用障碍同步的方式以硬件实现的全局同步是在可控的粗粒度级 ,从而提供了执行紧耦合同步式并行算法的有效方式,而程序员 并无过分的负担

  • BSP模型起到为软件和硬件之间架起一座类似于冯·诺伊曼机的 桥梁的作业,因此BSP模型也常叫做桥模型

  • BSP模型上曾直接实现了一些重要的算法(如矩阵乘、并行前序 运算、FFT和排序等),均避免自动存储管理的额外开销

  • 为PRAM模型所设计的算法,都可以采用在每个BSP处理器上模拟 一些PRAM处理器的方法来实现。

不足

  • 模型中,在超级步开始发送的消息,即使网络延迟时间比超级步 长度短,该消息也只能在下一个超级步才能被使用

  • 全局障碍同步假定是用特殊的硬件支持的,但很多并行机中可能 没有相应的硬件

LogP模型

分布存储、点到点的多处理机模型

模型描述

通信网络由4个主要参数描述

  • L:Latency,源处理机与目的处理机进行消息通信所需要的 等待或延迟时间的上限,表示网络中消息的延迟

  • O:Overhead,处理机准备发送或接收每个消息的时间开销

    • 包括操作系统核心开销和网络软件开销
    • 在这段时间里处理不能执行其它操作
  • G:Gap,一台处理机连续两次发送或接收消息时的最小时间 间隔,其倒数即微处理机的通信带宽。

  • P:Processor,处理机/存储器模块个数

以处理器周期为时间单位,Log可以表示成处理器周期 整数倍

特点

  • 抓住了网络与处理机之间的性能瓶颈:带宽

    • g反映了通信带宽,单位时间内最多有L/g个消息能进行处理机间传送。
  • 处理机之间异步工作,并通过处理机间的消息传送来完成同步

  • 对多线程技术有一定反映。每个物理处理机可以模拟多个虚拟 处理机(VP)

    • 某个VP有访问请求时,计算不会终止
    • VP的个数受限于通信带宽和上下文交换的开销、网络容量
    • 至多有L/g个VP。
  • 消息延迟不确定,但延迟不大于L

    • 消息经历的等待时间是不可预测的
    • 但在没有阻塞的情况下,最大不超过L。
  • 可以预估算法的实际运行时间。

不足

  • 对网络中的通信模式描述的不够深入,有些现象未描述、考虑

    • 重发消息可能占满带宽
    • 中间路由器缓存饱和等未加描述
  • 主要适用于消息传递算法设计

    • 对于共享存储模式,则简单地认为远地读操作相当于两次 消息传递
    • 未考虑流水线预取技术、Cache引起的数据不一致性以及 Cache命中率对计算的影响
  • 未考虑多线程技术的上下文开销

  • 用点对点消息路由器进行通信,这增加了编程者考虑路由器上 相关通信操作的负担

背景

  • 根据技术发展的趋势,20世纪90年代末和未来的并行计算机发展 的主流之一是巨量并行机,即MPC(Massively Parallel Computers), 它由成千个功能强大的处理器/存储器节点,通过具有有限带宽 和相当大的延迟的互连网络构成。所以我们建立并行计算模型 应该充分考虑到这个情况,这样基于模型的并行算法才能在现有 和将来的并行计算机上有效的运行。

  • 根据已有的编程经验,现有的共享存储、消息传递和数据并行 等编程方式都很流行,但还没有一个公认的和占支配地位的编程方式, 因此应该寻求一种与上面的编程方式无关的计算模型。而根据 现有的理论模型,共享存储PRAM模型和互连网络的SIMD模型对 开发并行算法还不够合适,因为它们既没有包含分布存储的情况, 也没有考虑通信和同步等实际因素,从而也不能精确的反映运行 在真实的并行计算机上的算法的行为,所以,1993年D.Culer等人 在分析了分布式存储计算机特点的基础上,提出了点对点通信 的多计算机模型,它充分说明了互联网络的性能特性,而不涉 及到具体的网络结构,也不假定算法一定要用现实的消息传递 操作进行描述

并行算法基本设计策略

串改并

发掘和利用现有串行算法中的并行性,直接将串行算法改造为并行 算法

  • 最常用的设计思路但并不普适
  • 好的串行算法一般无法并行化(数值串行算法可以)

全新设计

从问题本身描述出发,不考虑相应的串行算法,设计全新并行算法

借用法

找出求解问题和某个已解决问题之间的联系,改造或利用已知算法 应用到求解问题上

并行算法常用设计技术

划分设计技术

使用划分法把问题求解分成两步:

  • 把给定问题划分成p个几乎等尺寸的子问题
  • 用p台处理器并行求解子问题

分治设计技术

  • 将复杂问题划分成较小规模特性相同的子问题
  • 且子问题类型和原问题类型相同
  • 通常用递归完成分治算法

平衡树设计技术

  • 以树的叶结点为输入,中间结点为处理结点
  • 由叶向根或由根向叶逐层进行并行处理

倍增设计技术

  • 递归调用时,所要处理数据之间的距离逐步加倍
  • 经过k步后即可完成距离为2^k的所有数据的计算

流水线技术

  • 将算法路程分成p个前后衔接的任务片段,一个任务片段完成后 ,其后继任务片段可以立即开始

  • 则可以引入流水线的思想来处理多条数据

并行计算机体系架构

Shared Memory

shared_mem_image

Distributed Memory

distributed_mem_image

Hybrid

hybrid_mem_image

并行编程模型

特征 数据并行 共享变量 消息传递
代表 HPF OpenMP MPI、PVM
可移植性 SMP、DSM、MPP SMP、DSM 所有流行并行计算机
并行力度 进程级细粒度 线程级细粒度 进程级粗粒度
并行操作方式 松散同步 异步 异步
数据存储 共享存储 共享存储 分布式存储
数据分配方式 半隐式 隐式 显示
难度 较简单 简单
可扩展性 一般 较差

数据并行模型

相同操作同时作用于不同数据

共享变量模型

用共享变量实现并行进程间通信

消息传递模型

驻留在不同节点上的进程通过网络传递消息相互通信,实现进程之间 信息交换、协调步伐、控制执行等

?