Backgound
信号量 semaphores
,是操作系统中非常重要的技术,通过使用一个简单的整数值来管理并发进程,信号量只是一个在线程之间共享的整数变量。该变量用于解决临界区问题并实现进程同步。 信号量具有两个原子操作:
P()
:sem减一,如果sem<0,等待;否则继续V()
:sem加一,如果sem≤0,唤醒一个等待的P;
Semaphore
信号量的使用
型号量的特点:
两个类型信号量
二进制信号量
Binary Semaphore
:也称为互斥锁。它只能有两个值0和1。它的值被初始化为1。它用于实现多进程临界区问题的解决。计数信号量
Counting Semaphore
:值可以跨越一个不受限制的域(可以取任何非负数)。它用于控制对具有多个实例的资源的访问。
信号量是被保护的变量
初始化完成后,唯一改变一个信号量的值的办法是通过
P()
和V()
操作必须是原子
P()
能够阻塞,V()
不会阻塞
信号量可以用在2个方面
互斥
条件同步(调度约束 —— 一个线程等待另一个线程的事情发生)
信号量实现的互斥
|
|
信号量实现调度约束
|
|
信号量实现有界缓冲
在更复杂的同步场景下,用二进制信号量无法有效的解决问题,此时就需要计数信号量来完成这些功能;例如一个线程等待另一个线程处理事情
比如生产东西或消费东西(生产者消费者模式),互斥(锁机制)是不够的
有界缓冲区的生产者-消费者问题
- 一个或者多个生产者产生数据将数据放在一个缓冲区里
- 单个消费者每次从缓冲区取出数据
- 在任何一个时间只有一个生产者或消费者可以访问该缓冲区
在这里需要注意一些问题:
正确性要求
在任何一个时间只能有一个线程操作缓冲区(互斥);可多个
当缓冲区为空时,消费者必须等待生产者(调度,同步约束)
当缓存区满,生产者必须等待消费者(调度,同步约束)
每个约束用一个单独的信号量
- 一个二进制信号量,互斥
- 两个计数信号量
- 一般信号量 fullBuffers
- 一般信号了 emptyBuffers
|
|
管程
管程是一种解决进程间同步问题的程序结构,英文是 Monitors
;管程通过编程语言级别的支持,实现进程间的互斥。管程包含了一些列共享变量,以及针对变量的共享函数的组合,在设计上管程定义了:
- 锁
- 用锁来确保在任何情况下监视器中只有一个进程。
- 对共享数据提供互斥
- 0或者多个条件变量,用于管理对共享数据的并发访问
- 通过使进程进入睡眠状态的同时释放它们的锁,使线程在临界区内进入睡眠状态。
如下图所示:monitor是一种数据结构,用于将所有控制信息、时间信息和共享数据封装为一个整体。这个整体是对信号量的抽象,可以在其中定义互斥的控制语句。
进入管程需要有队列(entry queue),是互斥的,首先要获得lock
进入临界区后,执行函数对共享变量进行操作,这是在条件变量中
lock
Lock::Acquire()
等待直到锁可用,然后抢占锁Lock::Release()
释放锁,唤醒等待者如果有
Condition Variable
- 允许等待状态进入临界区
- 允许处于等待(睡眠)的线程进入临界区
- 某个时刻原子释放锁进入睡眠
Wait()
operation- 暂停对任何条件变量执行等待操作的进程。挂起的进程被放置在该条件变量的块队列中。
Signal()
operation(or broadcast() operation)- 唤醒等待的进程,需要进程存在
- 允许等待状态进入临界区
|
|
使用monitor,抽象出一个管程,并用word满足管程的锁和条件变量,word将计时器和控制信息聚合了,只有初始化时的结构才能获得锁:
- **Wait() **:如果定义了成员变量,则等待函数获取互斥锁
- **Signal() **:释放获取的锁,以便其他线程可以获取它。
|
|
Reference
https://www.cs.utexas.edu/users/lorenzo/corsi/cs372h/07S/notes/Lecture12.pdf
question of synchronize
Readers-Writers问题
Readers-Writers Problem
是允许多个进程读临界区,多个写者修改临界区;该问题的约束:
- 允许同一时间有多个读者,但在任何时候只有一个写者
- 没有写者时,读者才能访问数据
- 没有读者和写者时,写者才能访问数据
- 在任何时候只能有一个线程可以操作共享变量
读进程
|
|
上面代码 mutex
和 wrt
是信号量,rc
是读进程计数器,初始化时为0
写进程
|
|
基于管程的实现
|
|
其他实现方式
通常情况下为了保证读写问题,一般会通过互斥或信号量来实现。然而,go中提供了读写锁 sync.RWMutex
是为了解决这个问题的一种数据结构。
|
|
哲学家就餐问题
哲学家就餐问题 dining philosophers problem
;有五位哲学家,餐厅中间是一张圆桌,但只有五根筷子,每次吃饭需要两根筷子;当哲学家饿了就会拿起离自己最近的两根筷子;如果可以同时拿起离自己最近的两根筷子吃饭,吃完饭后,放下筷子思考。
如何设计
如图所示首先,哲学家们处于的状态 思考——拿筷子——吃饭——放下筷子——思考的状态中变化。
吃就是对临界区的访问,而如何拿筷子才是重点问题,而筷子则是 整个问题中的 Race Condition;可以将每根筷子互斥锁保护的共享对象,而在吃饭前,对其左右筷子进行加锁,两把锁都加成功,视为可以吃饭(访问临界区),吃完饭解锁筷子,进行思考
共享数据有:
- Bowl of rice(data set)
- Semaphone chopsticks [5] initialized to 1
步骤:
think()
:pickUpChopsticks()
:eating()
PutDownChopsticks()
|
|
这样在哪左边筷子完成时,再拿右边筷子时发现都被占用拿不到,而又不满足吃饭条件,导致死锁。为了防止死锁问题需要进一步的判断
|
|
互斥访问,可以解决但是同时只能一个哲学家就餐;这里将就餐看为临界区,而不是筷子,会造成筷子资源的浪费
|
|
为了防止死锁的发生,可以设置两个条件:
- 必须同时拿起左右两根筷子;
- 只有在两个邻居都没有进餐的情况下才允许进餐。
- 这种就诞生了一个原则:要么不拿,要么拿两个:
- step1:thinking
- step2:Hungry
- step3:左右邻居正在就餐则等待,否则下一步
- step4:拿起两个筷子
- step5:eating
- step6:方向左边筷子
- step7:方下右边筷子
- step8:to step1
|
|
具体的实现:
|
|
可以从执行结果看到,同时满足左右筷子都可以拿起的哲学家才可以进程吃
Reference