1、什么是独立线程、合作线程?
(1)独立线程:不与其它线程共享资源或状态
具有确定性和重现性,也就是输入相同的内容,保证得到预期的结果,整个线程执行过程中,不受外界影响
(2)合作线程:多个线程共享资源
也就会导致出现不确定性、不可重现,还可能出间歇性出现bug。
因为线程一般都是完成单一的任务,有些复杂的功能需要多个线程合作才能完成
2、进程或线程的合作有哪些优点呢?
3、案例分析:
如果是单一进程,这种一个进程分配一个id,id自增的机制没有什么问题
但是如果多个进程同时对id进行修改,就可能出现问题:
对于此案例,调度的位置具有随机性,在执行id修改指令时,如果产生了上下文切换,其他进程来进行id修改,但是上一个进程的id没有变,所以两个进程得到的新id是相同的。也就意味着最终的结果具有不确定性 和 不可重复性。
我们是不希望出现这样的结果的,所以我们要满足以下两个条件:
(1)无论多个线程的指令序列怎样交替执行,程序都必须正常工作
(2)不确定性要求并行程序的正确性
4、为什么会出现不确定、不可重现的现象?
因为程序处于竞态条件,进程结果依赖于并发执行或者时间的顺序/时间。
5、那么如何去避免竞态呢?
(1)指不会被线程调度机制打断的操作【整个原子操作不能被打断】
(2)要么完全执行,要么就不执行【操作的最小单位】
实际进行一次加法或减法是三条指令构成,所以在没有严格划分临界区时,这个案例的结果是不确定的
6、引入几个其他概念:
1、临界区有哪些属性?
(1)互斥:同一时间临界区中最多存在一个线程
(2)前进:如果一个线程想要进入临界区,那么它最终会成功
(3)有限等待:如果一个线程i处于入口区,那么在i请求被接受之前,其他线程进入临界区的时间是有限制的
(4)无忙等待(可选):如果一个进程在等待进入临界区,那么在它可以进入之前会被挂起
2、是否可以设计一些方法对临界区代码的保护呢?
方法1:禁用 硬件中断
方法2:基于软件的解决方法
方法3:更高级的抽象【硬件原子操作的一种指令】
我们将从性能和实现的复杂性来比较这几种方法
3、禁用硬件中断时如何实现的呢?
基于:中断被禁用后就不会进行上下文切换,进而就不会有并发的进程
主要就是通过进入临界区禁用中断,离开临界区开启中断
存在的缺点:
(1)中断的作用是用于相应外部设备的时间,如果禁用中断之后,整个系统都会停下来,也就可能导致其他线程处于饥饿状态
(2)如果临界区任意长,那么就会导致无法限制响应中断所需的时间【对系统影响很大】
(3)所以我们应该小心使用,适用于临界区很小的情况,不适用多CPU的情况
1、如何来实现基于软件的解决方案呢?
//设置共享变量,初始化
int turn = 0
//turn 为几就为哪个进程进入临界区
turn == i
//Thread Ti
do{
//如果没有临到进程i访问临界区就一直等待
while(turn != i);
critical section //执行临界区的代码
turn = j; //轮到j可以访问临界区的资源了
reminder section //进程i执行剩余的低吗
}while(1);
算法分析:
(1)可以满足进程互斥
(2)但是无法满足前进(谁想访问临界区最后就一定访问的到临界区)
如果进程0在等待临界区访问权限太久,可能就不再想去访问临界区,当进程1访问完临界区,把权限给到进程0,但是进程0不想访问临界区了,但是进程1还想访问临界区,就导致进程1想访问临界区但是却达不到目的
算法分析:
尽管实现了前进,但是不满足互斥,最开始进程都为0,都可以进入临界区
算法分析:
满足了互斥,但是存在死锁,在进程开始时flag标志都会变为1,则会都阻塞在循环这里
2、Perterson算法实现原理:
结合以上三种方案,采用了两个共享数据项
int turn; //指示谁该进入临界区
boolean flag[]; //指示进程是否准备好进入临界区
进入临界区部分代码
flag[i] = TRUE;
turn = j;
//此处说明进程j在访问临界区,所以阻塞进程i[属于通过别人的状态来考虑自己]
while(flag[j] && turn == j);
退出临界区部分代码
//与上面进入临界区的代码是对应的
flag[i] = FALSE;
完整代码
3、 Dekker算法(了解):
4、 针对N个进程的互斥保护——Eisenberg and McGuire算法:
5、 针对N个进程的互斥保护——Beakery算法:
(1)在进程进入临界区之前会获得一个编号,谁编号小谁先访问临界区,如果两个进程接收到的数字相同,那么谁id小谁先访问
(2)编号方案是按照枚举增加顺序生成数字
(1)一个二进制状态,有锁定和解锁两种方法
(2)Acquire(): 锁被释放前一直等待,然后得到锁【锁没释放,处于临界区之外的进程无法访问临界区】
(3)Release():释放锁,可以唤醒任何等待的进程
lock_next_pid -> Acquire();
new_pid = next_pid++;
lock_next_pid -> Release();
(1)大多数现代体系结构都提供特殊的原子操作指令
通过特殊的内存访问电路,针对单处理器和多处理器
(2)Test-and-Set 测试和置位指令
从内存中读取值,测试该值是否为1(然后返回真或假)、内存值设置为1
boolean TestandSet(boolean *target){
boolean rv = *target;
*target = true;
return rv;
}
(3)交换指令:交换内存中的两个值
void Exchange(boolean *a, boolean *b)
{
boolean temp = *a;
*a = *b;
*b = temp;
}
2、基于TS指令实现自旋锁(spinlock)
(1)使处于忙等的进程睡眠,在临界区执行完的进程将睡眠的进行唤醒
(2)根据临界区的执行时间长短决定采用哪种方式
如果临界区执行时间短 >> 忙等待
如果临界区执行时间长 >> 无忙等待
7、基于Exchange指令实现:
//初始化共享数据
int lock = 0;
//当前线程为 Ti
int key;
do{
key = 1;
while(key == 1) exchange(lock, key);
//临界区
critical section
lock = 0;
//剩余部分
remainder section
}while(1)
8、原子操作指令锁的特征:
(1)优点
(2)缺点
9、总结:
(1)基于硬件的原子操作可以利用锁来实现线程/进程之间的互斥
(2)基于禁用中断的锁更适用于单处理器、软件实现的锁比较复杂、更推荐硬件原子操作实现的锁
(3)可采用有忙等待和无忙等待两种方案