线程同步技术
可以参考线程同步技术
进程同步技术
进程间同步技术常常利用锁和信号量来解决,而锁页分为悲观锁和乐观锁。
- 悲观锁:先加锁再操作临界区,适用于冲突概率高的情况
- 乐观锁:先操作临界区,再检查是否发生了冲突,发生冲突就需要回溯,只适用于冲突概率极低的情况,因为回溯的成本是巨大的
通过pthread_mutex_*
等函数来实现,锁一般会放到共享内存中去
一些进程同步模型
生产者消费者模型
存在一个缓冲区,生产者往里面放数据,消费者从里面拿数据,如何保证它们同步
- 使用一个空槽信号量和一个满槽信号量,分别用来表示缓冲区空闲位置数和非空闲位置数
- 一把全局锁,用来限定对缓冲区的临界访问
- 生产者需要消耗(P)空槽资源,释放(V)满槽资源
- 消费者需要消耗(P)满槽资源,释放(V)空槽资源
伪代码如下:
constexpr int N = 100; // 缓冲区大小
semaphore emptyBuffers = N; // 初始定义称缓冲区的空槽数量
semaphore fullBuffers = 0; // 初始定义为缓冲区的满槽数量
semaphore mutex = 1; // 定义锁
// 生产者
void producer(){
while(1){
P(emptyBuffers);
P(mutex);
// 生产
V(mutex);
V(fullBuffers);
}
}
// 消费者
void consumer(){
while(1){
P(fullBuffers);
P(mutex);
// 消耗
V(mutex);
V(emptyBuffers);
}
}
哲学家进餐模型
哲学家进餐模型为IO设备等有限竞争问题提供了模型。
在一张圆桌上面,有五个哲学家以及五把叉子(扩展到N个哲学家和N把叉子),哲学家和叉子相间分布,每个哲学家可以拿到左右手的叉子。哲学家围坐在一起思考,此时不需要叉子,思考饿了哲学家想要进餐,哲学家必须拿到两边的叉子才能进餐,只有进餐完毕才会把叉子放回原处。
解决方案有如下:
选择五个信号量,每个哲学家进餐会尝试去P左边的叉子,然后P右边的叉子
sem fork[N]; while (1){ think(); // 思考 P(fork[i]); // 左边的叉子 P(fork[(i+1)%N]); // 右边的叉子 eat(); // 进餐 V(fork[(i+1)%N]); // 右边的叉子 V(fork[i]); // 左边的叉子 }
不足在于:如果每个哲学家同时P左边的叉子,那么就死锁了
添加一个全局锁,先P锁,然后再P左叉子,最后P右叉子,然后进餐
sem fork[N]; sem mutex = 1; while(1){ think(); // 思考 P(mutex); P(fork[i]); // 左边的叉子 P(fork[(i+1)%N]); // 右边的叉子 eat(); // 进餐 V(fork[(i+1)%N]); // 右边的叉子 V(fork[i]); // 左边的叉子 V(mutex); }
不足在于:其实自始至终只有一个哲学家能进餐,效率低下
参考解决方案1,只有同时拿起一边的叉子才会死锁,那么设定奇数偶数拿起叉子的顺序不一致可解决
sem fork[N]; while (1){ think(); // 思考 if(index % 2 == 0){ // index是哲学家的编号 P(fork[i]); // 左边的叉子 P(fork[(i+1)%N]); // 右边的叉子 eat(); // 进餐 V(fork[(i+1)%N]); // 右边的叉子 V(fork[i]); // 左边的叉子 }else{ P(fork[(i+1)%N]); // 右边的叉子 P(fork[i]); // 左边的叉子 eat(); // 进餐 V(fork[i]); // 左边的叉子 V(fork[(i+1)%N]); // 右边的叉子 } }
通过哲学家的状态来判定,将哲学家分为进餐状态,思考状态,饥饿状态(试图拿叉子),只有当左右两个邻居都没进餐才可进餐。
读者-写者模型
读者-写者模型为数据库访问提供了模型
其基本描述为:可以多个同时读,但是只要有写的,就会独占,即
- 读-读:允许,同一时刻,多个读者可读
- 读-写:互斥,同一时刻,有写就不能读
- 写-写:互斥,同一时刻,只能一个可写
解决方式会分成,读者优先策略,写者优先策略以及公平策略
- 读者优先:只要有读者正在读,那么后来读者可进入,如果读者数为0,写者才能进入。这样会导致写者饥饿
- 写者优先:只要有写者要写入,那么读者必须等待,直到全部写者写入完成
- 公平策略:优先级相同,读写互斥,只能一个写者到达临界区,可以多个读者到达临界区
死锁避免
死锁只有满足四个条件才会发生:
- 互斥条件:多个线程不能同时拥有一种资源
- 持有并等待条件:一个线程有了资源1,还要获取资源2,但是资源2正在被别的线程获取了
- 不可剥夺条件:资源只能由占用线程释放,其他线程不能释放
- 环路等待:死锁发生时,两个线程获取资源的顺序构成环路,即线程-资源-线程-资源环路
避免死锁的方式只需要破坏上面四种条件中的一种即可:
- 资源顺序获取:即所有线程都应该按照获取资源1,资源2…的顺序来获取,破坏环路等待
- 一次性申请资源:所有进程在一开始都申请全部资源,破坏持有等待
- 及时释放资源:如果占用资源的线程再次获得其他资源没有被满足,应该及时释放已获取资源,破坏不可剥夺
银行家算法
银行家算法时迪杰斯特拉提出的一种解决动态资源分配的算法,其核心在于,通过安全算法估量是否分配资源。
具体表现为:
数据结构:银行家算法需要存储以下几个数据结构
Avaliable
向量:表示当前每种资源可使用数量Max
矩阵:每个进程对每种资源的最大数量Allocation
矩阵:每个进程已获得每种资源的数量Need
矩阵:每个进程还需要获得每种资源的数量Work
向量:初始化为Avaliable
向量Finish
向量:初始化为False
执行过程:假设第$i$个进程开始申请资源,资源数量的向量为
Request[i]
,并对第$j$种资源做以下判断Request[i][j]<=Need[i][j]
,否则出错,是则继续Request[i][j]<=Avaliable[j]
,否则等待,是则继续尝试分配
Avaliable[j] -= Request[i][j]; Allocation[i][j] += Request[i][j]; Need[i][j] -= Request[i][j];
安全性检查,是则分配且继续,否则回退且阻塞
安全性检查:
- 检查第
i
个进程,查看是否满足Finish[i]==flase and Work>=Need
。能找到i
执行2,不能找到这样的i
执行3 Work+=Allocation[i], Finish[i]=true
,执行2- 假如所有进程
Finish==true
则安全,否则不安全
- 检查第