以多线程观点理解
并发程序
从上述种种原因,我们可以看出,并发程序每次运行的结果不能保证都相同,这是由于并发程序的非封闭性造成的,而这也是我们需要解决的问题——只有可在现的结果才是正确的。
如何才能保持程序的可再现性呢?
我们说,当两段程序之间的读写集合互不相交,其写集合也互不相交时,就能保证可再现性。
1. 背景 首先,看个例子,进程P1,P2共用一个变量COUNT,初始值为0


进程互斥
进程互斥在并发编程时是特别需要注意的,否则就会因为并发程序的非封闭性而造成无可挽救的损失。
进程互斥是进程之间所发生的一种间接相互作用,是进程本身所不希望的,也是运行进程感受不到的。
在继续向下讲述前,先解释几项概念:
1. 共享变量
多个进程均需访问的变量。
可以近似的理解为高级语言中的全局变量或静态变量(因为这两种变量类型是多个程序段或函数均可访问的)。
另外需要注意:共享变量既可能属于操作系统空间,也可能属于用户进程空间。
临界区
访问共享变量的程序段。进程互斥
多个进程不能同时进入关于同一组共享变量的临界区,否则可能发生与时间有关的错误。由于互斥是操作系统乃至并发程序设计中十分重要的概念,故需要准确的理解互斥的概念。
不允许多个进程同时进入关于同一组共享变量的不同临界区
不允许多个进程同时进入关于同一组共享变量的相同临界区
.临界资源
空闲让进
| 当无进程处于临界区内时,必须让一个要求进入临界区的进程立即进入,以有 效地利用临界资源。 |
忙则等待
| 当已有进程处于临界区内时,其它试图进入临界区的进程必须等待,以保证它 们互斥地进入临界区。 |
有限等待
| 对要求进入临界区的进程,应在有限时间内使之进入,以免陷入“死等”。 |
让权等待
| 对于等待进入临界区的进程而言,它必须立即释放处理机,以免进程“忙等” |
实现进程互斥
实现互斥,就是保证同一时刻最多只有一个进程处于临界区内,也即实现对于临界区的管理。
需要满足如下几个正确性原则:
1. 互斥性原则
任意时刻之多只能有一个进程处于关于同一组共享变量的临界区之中。
2. 进展性原则
临界区空闲时,只有执行了临界区入口及出口部分代码的进程参与下一个进入临界区的决策,该决策不可无限期延迟。
3. 有限等待性原则
请求进入临界区的进程应该在有限的等待时间内获得进入临界区的机会。
Dekker互斥算法
算法核心思想就是:
- 设置一个可以表示进程是否将要进入或已经处于临界区的标志位。
- 某一进程要进入临界区,需要查看其它进程是否处于临界区(检查对应进程号的标志位是否为1),做出等待或进入临界区的动作。
Peterson互斥算法
算法核心思想为:
- 只要第二个进程在临界区,第一个进程就等待。
相较于Dekker算法简单
public class Peterson implements Runnable {
private static boolean[] in = { false, false };
private static volatile int turn = -1;
public static void main(String[] args) {
new Thread(new Peterson(0), "Thread - 0").start();
new Thread(new Peterson(1), "Thread - 1").start();
}
private final int id;
public Peterson(int i) {
id = i;
}
private int other() {
return id == 0 ? 1 : 0;
}
@Override
public void run() {
in[id] = true;
turn = other();
while (in[other()] && turn == other()) {
System.out.println("[" + id + "] - Waiting...");
}
System.out.println("[" + id + "] - Working ("
+ ((!in[other()]) ? "other done" : "my turn") + ")");
in[id] = false;
}}
Lamport面包店算法
算法核心思想为:
- 对每个进程增设一个“摇号”状态,以及摇出的号码。
- 进程处于“摇号”状态时不被获准进入临界区。
- 对于“摇号”结束的进程进行所持有号码的比较,小号先行原则。
算法思想源于面包店,但是这种情况确实经常会经历的。
现在支付宝,微信用的比较多,以前去银行存钱,每次都得在门口的柜台机取一个号码,等柜员叫号,有时候银行里明明没什么人,取号的时候却显示有xx人在我前面,那叫一个气哦,所以在银行等的时候就很无聊了,时不时的要注意一下柜员有没有叫到自己。
所以每次看到那种拿金卡去后面办业务的人好羡慕……
银行取号排队的这个过程可能是Lamport算法最好的一个实例(天知道这些算法是不是哪个人在买面包的时候想到的)。
Eisenberg-Mcguire算法
个人觉得该算法是Dekker算法的加强版本,这算法的核心思想是:
- 设置一个标志位表示空闲,准备进入,已进入三种状态。并标记当前正在临界区的进程号。(是不是很像Dekker算法呢)
- 某一进程想要进入临界区时,判断是否存在其他进程已在临界区。
- 退出临界区时找到下一个非空闲的进程。
#define true 1 #define false 0 #define process_num 4//线程数目 int choosing[process_num]={false}; int number[process_num]={0}; int find_max(void)/*找出最大号码*/ { int max=0; int i=0; for(;i<process_num;++i) { if(number[i]>max) max=number[i]; } return max; } void enter_lock(int thread_id) { int i=0; choosing[thread_id]=true; number[thread_id]=find_max()+1;/*选号码*/ choosing[thread_id]=false; for(;i<process_num;++i) { while(choosing[i]);/*等待其他线程选号码*/ while((number[i] != 0)&& ( (number[i] < number[thread_id]) || ((number[i] == number[thread_id]) && (i < thread_id)) ));/*阻塞,等待调度*/ } } void exit_lock(int thread_id) { number[thread_id]=0;/*释放号码*/ }
void process_A(void)//线程0 { enter_lock(1); //临界区 //访问资源 exit_lock(1); } void process_B(void)//线程1 { enter_lock(2); //临界区 //访问资源 exit_lock(2); }
我们发现,在Lamport和Eisenberg算法中出现了一条循环语句,可以让进程处于“等待”状态,但这并不是让进程从运行态变为等待态,只是让活动进程执行循环直到条件满足才能跳出而已,并不涉及到进程的状态改变。
这种等待状态也称“忙式等待”。
“测试并设置”指令
前文所介绍的都是软件方法实现进程的互斥,但硬件实现进程互斥通常会比采用软件方法要简单,不需要过多的动作,不过这需要以硬件支持为前提。
“测试并设置”指令的定义如下:
int test_and_set(int *target)
{
int temp;
temp = *target;
*target = 1;
return temp;
}
这条指令是原子的,即在指令执行时是不可分割的。
基于ts指令的算法思想与Dekker算法较为类似,为:
- 查看现在是否有进程处于临界区。
左边的算法非常简单,但是却不满足有限等待性,即公平性。
当某一进程退出临界区时,该算法没有指定下一个可以进入临界区的进程,如此便可造成一些进程长时间处于等待状态,甚至出现饿死的情况,即为不公平的算法。
右边的算法则稍显复杂,但主要只增设了两个标志位,waiting表示是否处于等待态,key表示是否被获准进入临界区。
由于出现了waiting这一标志位,因而现在可以得到多个进程的状态用于判断是否可以指定下一个可以进入临界区的进程。
“交换”指令
交换指令也是原子的,其定义为:
void swap(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
交换和测试并设置指令的算法都是套路……
硬件提供中断指令:
do{
非临界区代码;
关中断;
临界区;
开中断;
其余代码;
}while(1);
转载·于:https://blog.csdn.net/ai977313677/article/details/72411488