目录 |
- - 1. 一些概念 - 2. 两种经典的软件解法 - 严格轮换 - 著名的PETERSSON解法 - 3. 原子操作 - 4. 睡眠唤醒 -> 信号量、互斥量 -> 管程 - 引例:睡眠与唤醒 - 信号量、互斥量 - 信号量实现的生产者消费者模型 - 管程
|
1. 一些概念
- 临界区,对共享内存进行访问的程序片段称为临界区。
- 忙等待的互斥(自旋等待),自旋锁,基于忙等的锁
2. 两种经典的软件解法
严格轮换
1// 进程a 2while(TRUE){ 3 while(turn != 0); /* 循环测试turn,看其值何时变为0 */ 4 critical_region(); /* 进入临界区 */ 5 turn = 1; /* 让给下一个进程处理 */ 6 noncritical_region(); /* 离开临界区 */ 7} 8// 进程b 9while(TRUE){ 10 while(turn != 1); /* 循环测试turn,看其值何时变为1 */ 11 critical_region(); /* 进入临界区 */ 12 turn = 0; /* 让给下一个进程处理 */ 13 noncritical_region(); /* 离开临界区 */ 14} |
一个阻塞,两个都阻塞,上述未表明的关键是,进去临界区的代码如何实现。
著名的PETERSSON解法
1#define FALSE 0 2#define TRUE1 3#define N 2 /* 进程数量 */ 4 5int turn; /* 现在轮到谁? */ 6int interested[N]; /* 所有值初始化为0 (FALSE) */ 7 8void enter_region(int process){ /* 进程是0或1 */ 9 int other; /* 其他进程号 */ 10 other = 1 - process; /* 另一方进程 */ 11 interested[process] = TRUE; /* 表示所感兴趣 */ 12 turn = process; 13 while(turn == process && interested[other] == TRUE);/* 空循环 */ 14} 15 16void leave_region(int process){ 17 interested[process] = FALSE; /* 表示离开临界区 */ 18} |
3. 原子操作
类似的指令都是用硬件保证原子性,例如,几个命令之间屏蔽中断,由于几个语句都是非常快的,所以整体非常快。
- 测试并设置锁 Test and Set Lock (TSL)
- 获取并增加 Fetch-and-Increment
- 交换 Swap
- 比较并交换 Compare-and-Swap (CAS)
- 加载链接/条件存储 Load-linked / Store-Conditional LL/SC
避免忙等问题,现在大部分都是采用睡眠唤醒机制。还有更多基于上述指令实现的复杂指令。
- 睡眠与唤醒(线程挂起)
然后基于睡眠和唤醒构建的例如。
- 信号量与互斥量
再次,基于信号量和互斥量构建的的。
- 管程
4. 睡眠唤醒 -> 信号量、互斥量 -> 管程
理解信号量、互斥量概念非常重要,因为大部分机制都是由这些东西实现的。
那么,什么是信号量、互斥量,它们的本质到底是什么?
引例:睡眠与唤醒
1#define N 100 /* 缓冲区中的槽数量 */ 2int count = 0; /* 缓冲区中的数据项数目 */ 3 4// 生产者 5void producer(void){ 6 int item; 7 8 while(TRUE){ /* 无限循环 */ 9 item = produce_item() /* 产生下一新数据项 */ 10 if(count == N) sleep(); /* 如果缓冲区满了,就进入休眠状态 */ 11 insert_item(item); /* 将新数据放入缓冲区中 */ 12 count = count + 1; /* 缓冲区数据项计数器+1 */ 13 if(count == 1) wakeup(consumer); /* 缓冲区不为空则唤醒消费 */ 14 } 15} 16 17// 消费者 18void consumer(void){ 19 int item; 20 21 while(TRUE){ /* 无限循环 */ 22 if(count == 0) sleep(); /* 如果缓冲区是空的,则进入休眠 */ 23 item = remove_item(); /* 从缓冲区中取出一个数据项 */ 24 count = count - 1 /* 将缓冲区的数据项计数器-1 */ 25 if(count == N - 1) wakeup(producer); /* 缓冲区不满,则唤醒生产者? */ 26 consumer_item(item); /* 打印数据项 */ 27 } 28} |
这是生产者消费者的典型实现,但是它是不安全的。为什么?考虑一种情况:
假如从 if(count == N) 这里被中断了,此时生产者为睡眠,而切换到消费者,消费者执行,消费了一个,count=N-1,则调用wakeup,问题是,生产者并没有睡眠,于是wakeup丢失。这明显就出问题了,比如,回到生产者,生产者被睡眠了,而此时,若N-1=0,则消费者也睡眠。
有些复杂,问题到底出在哪里?其实非常简单,核心问题是,count的判断和睡眠,不是原子操作。——判断操作之后可以中断的话,是引起各种问题的根源,你看,原子操作中就是将测试操作原子化的。
所以,解决并发问题的本质——增加原子操作范围,不够就增,不够就再增,就这么简单。
信号量、互斥量
为了改进上述生产者,消费者模型,引入信号量和互斥量。
其实都是由原子操作构成的,简单得很。
- down:可以用sleep来表示,如果此刻信号量大于0,则将其值-1,如果=0,则进入进程进行睡眠;
- up:可以用wakeup来表示,up操作会使信号量+1,如果因为多个进程睡眠而无法完成先去的down操作,系统会选择一个进程唤醒并完成down操作,但信号量值仍是0,取而代之的是睡眠进程数量减1;
来自 <https://zhuanlan.zhihu.com/p/109971253>
总之,信号量和互斥量都是原子化的测试+赋值+睡眠+唤醒。它们的实现就如同所有的原子操作一样。
信号量原理:
检查数值、修改变量值以及可能发生的休眠或者唤起操作是原子性的,通常将up和down作为系统调用来实现;
当执行以下操作时,操作系统暂时屏蔽全部中断:检查信号量、更新、可能发生的休眠或者唤醒,这些操作需要很少的指令,因此中断不会造成影响;
如果是多核CPU,信号量同时会被保护起来,通过使用TSL或者XCHG指令确保同一个时刻只有一个CPU对信号量进行操作。
来自 <https://zhuanlan.zhihu.com/p/109971253>
信号量实现的生产者消费者模型
1#define N 100 /* 缓冲区中的槽数目 */ 2typedef int semaphore; /* 信号量是一种特殊的整型数据 */ 3semaphore mutex = 1; /* 控制对临界区的访问 */ 4semaphore empty = N; /* 计数缓冲区的空槽数目 */ 5semaphore full = 0; /* 计数缓冲区的满槽数目 */ 6 7void producer(void){ 8 9 int item; 10 11 while(TRUE){ /* TRUE是常量1 */ 12 item = producer_item(); /* 产生放在缓冲区中的一些数据 */ 13 down(&empty); /* 将空槽数目-1 */ 14 down(&mutex); /* 进入临界区 */ 15 insert_item(item); /* 将新数据放入缓冲区中 */ 16 up(&mutex); /* 离开临界区 */ 17 up(&full); /* 将满槽数目+1 */ 18 } 19} 20 21void consumer(void){ 22 23 int item; 24 25 while(TRUE){ /* 无限循环 */ 26 down(&full); /* 将满槽数目-1 */ 27 down(&mutex); /* 进入临界区 */ 28 item = remove_item(); /* 从缓冲区取出数据项 */ 29 up(&mutex); /* 离开临界区 */ 30 up(&empty); /* 将空槽数目+1 */ 31 consume_item(item); /* 处理数据项 */ 32 } 33} |
此时又有一个非常经典的问题——为什么有了信号量,还需要互斥量?它们的本质有什么区别?
有了上一节提到的理解,这里就非常简单了,你只需要考虑一下,若没有mutex互斥量,会如何?会导致down操作结束后,会被中断!此时又会出现类似上面的测试之后被中断的问题!
所以啊,为什么要有信号量和互斥量呢,其实信号量是对资源的计数,而互斥量则是资源为1的信号量,用来实现互斥区,即——修改资源的时候,我需要mutex来保证只有一个线程可以进去!
自此,核心概念结束,在我的理解中,管程,PV操作等,都是基于这些核心概念搞定的。
管程
只需要看一下概念就可以理解了,这个东西是一种封装的线程安全区域,是对原始机制的一种抽象。
管程是程序、变量和数据结构等组成的集合,构成一个特殊模块或者软件包,进程可以调用管程中的程序,但是不能在管程之外声明的过程中直接访问管程内的数据结构。
来自 <https://zhuanlan.zhihu.com/p/109971253>
看吧,核心就是多了程序,变量,数据结构,以及性质,只有一个线程在内。