当处理器的性能的发展受到各方面因素的限制的时候,计算机产业开始用多处理器结构实现并行计算来提高计算的效率。我们使用多处理器共享存储器的方式实现了多处理器编程,也就是多核编程。当然在这样的系统结构下我们面临着各种各样的挑战,例如如何协调各个处理器之间的数据调度以及现代计算机系统固有的异步特征等等。
在接下来的一系列文章中,我将会介绍一些基础的原理以及并行程序的设计和并发程序的设计及实现,写这篇文章是对近期学习课程的总结,方便自己温故实习,感谢USTC付明老师的《多核并行计算》课程,了解更多推荐《The Art of Multiprocessor Programming, Revised Reprint》。
互斥
临界区(Critical Section)
互斥是多处理器程序设计中常见的一种协作方式,互斥的定义:不同线程的临界区之间没有重叠。对于线程A、B以及整数j、k,或者
无死锁:如果一个线程正在尝试获得一个锁,那么总能成功地获得这个锁。若线程A调用lock()但无法获得锁,则一定存在其他的线程正在无穷次地执行临界区。
无饥饿:每一个试图获得锁的线程最终都能成功。每一个lock()调用最重都将返回。这种特性有时称为无封锁特性。
注意:无饥饿意味着无死锁。
双线程的解决方案
我们通过几个算法的实现类分析:
1 . LockOne类
class LockOne implements Lock{
private boolean[] flag = new booean[2];
//线程的标识为0或1
public void lock(){
int i = ThreadID.get();// 每个线程通过调用ThreadID.get()获取自己的标识。
int j = 1-i; //若当前调用者的标识为i,则另一方为j=1-i。
flag[i] = true;
while(flag[j]){} //wait
}
public void unlock(){
int i = ThreadID.get();
flag[i] = false;
}
}
LockOne算法满足互斥特性。
证明 假设不成立,考虑每个线程在第k次(第j次)进入临界区前最后一次调用lock()方法的执行情形。
通过观察代码可以看出
writeA(flag[A] = true)->readA(flag[B]==flase)->CSA(1)
writeB(flag[B] = true)->readB(flag[A]==false)->CSB(2)
readA(flag[B]==false)->writeB(flag[B]=true)(3)
当flag[B]被设置为true,将保持不变。公式(3)必须成立,否则线程A不可能读到flag[B]到值为false。
由公式(1)-(2)和先于关系的传递性可导出公式:
writeA(flag[A] = true)->readA(flag[B]==flase)->writeB(flag[B] = true)->readB(flag[A]==false)(4)
由此可以看到,从writeA(flag[A] = true)->readB(flag[A]==false)整个过程没有对flag[]进行写操作,也就是说B线程不可能读到flag[A]==false,得到了矛盾。
**LockOne算法的缺陷:**LockOne算法在交叉执行的时候会出现死锁。若事件writeA(flag[A] = true)与writeB(flag[B] = true)在事件readA(flag[B]==flase)和readB(flag[A]==false)之前发生,那么两个线程都将陷入无穷等待。
2 . LockTwo类
class LockTwo implements Lock{
private volatile int victim;
public void lock(){
int i = ThreadID.get();
victim = i;//let the other go first
while(victim == i){} //wait
}
public void unlock(){}
}
LockTwo算法满足互斥特性。
证明 假设不成立。考虑每个线程在第k次(第j次)进入临界区前最后一次调用lock() 方法的执行情形。
通过观察代码可以看出
writeA(victim = A)->readA(victim==B)->CSA(1)
writeB(victim = B)->readB(victim==A)->CSB(2)
线程B必须在事件writeA(victim = A)和事件readA(victim==B)之间将B赋值给victim域,由假设知道这是最后一次赋值,所以有
writeA(victim = A)->writeB(victim = B)->readA(victim==B)(3)
一旦victim域被设置为B,则将保持不变,所以,随后的读操作都是返回B,这将与公式(2)矛盾。
LockTwo类存在的缺陷:当一个线程完全先于另一个线程执行的时候就会出现死锁。但是两个线程并发地执行,却是成功的。由此,我们看到Lo