-
信号量的定义
- 信号量(semaphore) 是一个整数值的对象
- 信号量的初始值可以决定其行为,所以需要在使用信号量前为其赋初值
- 有两个函数可以用于操作信号量,分别是sem_wait() 和sem_post()
- sem_wait():
- 先将信号量的值-1
- 然后如果信号量的值<0,那么线程开始等待
- sem_post():
- 先将信号量的值+1
- 如果当前存在线程正在该信号量上等待,那么唤醒其中一个
- sem_wait():
- 当信号量的值为负数(-n) 时,就代表存在n个线程正在等待
-
信号量用作锁(二值信号量)
- 将信号量作为锁使用时,我们需要给信号量赋初值为1
- 假设有2个线程t0、t1,使用信号量s保护某个临界区:
- t0即将进入临界区,调用sem_wait(),传入信号量s
- s的值-1,变为0,t0正常进入临界区
- 此时若t1想进入临界区,就也需要调用sem_wait(),但接下来s的值变为-1,为负数,t1无法进入临界区,开始睡眠
- t0离开临界区,调用sem_post(),s的值+1变为0,同时唤醒一条等待着的线程
- t1被唤醒,得以进入临界区
- 在这种情况下,由于信号量只有两个状态:持有和没被持有,所以被称为二值信号量(binary semaphore)
-
信号量用作条件变量
- 信号量也可以用于让一个线程停止执行,等待另一条件成立,也就是用作条件变量
- 假设现在有进程中初始有一个线程,该线程(父线程)会创建一个子线程,然后等待子线程运行结束,子线程结束后父线程才结束,此时我们需要给信号量赋初值为0
-
父线程运行
- 父线程**创建子线程
- 让子线程开始运行
- 父线程调用sem_wait()
- 父线程从sem_wait()中醒来后,父线程结束
-
子线程运行
- 子线程开始
- 子线程调用sem_post(),并立即结束
-
两种运行情况:
- 第一种:父线程调用sem_wait()时,子线程还没有运行完毕
- 父线程创建子线程
- 子线程开始运行
- 父线程调用sem_wait(),此时信号量的值从0变为-1,为负数,父线程开始等待
- 子线程运行到sem_post(),将信号量的值从-1变为0,并唤醒正在等待的父线程,紧接着子线程结束
- 父线程被唤醒,然后父线程结束
- 第二种:父线程调用sem_wait()时,子线程已经运行完毕
- 父线程创建子线程
- 子线程开始运行
- 子线程很快就执行完毕了,调用sem_post(),将信号量的值从0变为1,并尝试唤醒正在等待的线程,此时没有线程正在等待,紧接着子线程结束
- 父线程调用sem_wait(),此时信号量的值从1变为0,不是负数,父线程不用等待,直接继续执行
- 父线程结束
- 第一种:父线程调用sem_wait()时,子线程还没有运行完毕
-
读者-写者锁
- 读者-写者锁(reader-writer lock) 用于实现一些更加灵活的并发操作
- 对于一块共享资源,读取内容是可以多个线程并发进行的
- 写入内容则必须保证只有一个线程正在访问这块资源,此时其他线程不能读或写
- 读者-写者锁(reader-writer lock) 用于实现一些更加灵活的并发操作
-
哲学家就餐问题
- 哲学家就餐问题(Dining philosopher’s problem) 是一个著名的并发问题,由迪杰斯特拉(Dijkstra)提出并解决
- 问题内容为:
- 假定有5位哲学家围坐在圆形餐桌上,每两位哲学家之间有一把餐叉(一共5把餐叉)
- 哲学家有时会思考,此时不需要餐叉
- 哲学家有时也会拿起餐叉吃饭
- 哲学家只有同时拿起左右边的两把餐叉,才能吃饭
- 问题中,哲学家将会有拿起左右手的叉子getforks()、吃饭eat()、放下左右手的叉子putforks() 这3个操作
- 实现getforks()、putforks() 这两个操作并保证没有死锁、没有哲学家饿死、并发度更高(能有更多的哲学家同时吃饭) 就是问题的目标
-
有问题的解决方案
- 给每个叉子都赋予一个信号量,给每个信号量都用1初始化
/* getforks() 让编号为p的哲学家拿起左手的叉子,再拿起右手的叉子 参数: p int类型,代表哲学家的编号 返回值: 无 */ void getforks(int p) { sem_wait(forks[left(p)]); // left(p)代表哲学家p左手边叉子对应的信号量 sem_wait(forks[right(p)]); // right(p)代表哲学家p右手边叉子对应的信号量 } /* putforks() 让编号为p的哲学家放下左手的叉子,再放下右手的叉子 参数: p int类型,代表哲学家的编号 返回值: 无 */ void putforks(int p) { sem_post(forks[left(p)]); // left(p)代表哲学家p左手边叉子对应的信号量 sem_post(forks[right(p)]); // right(p)代表哲学家p右手边叉子对应的信号量 } - 这种方案会有死锁问题,当每个哲学家都拿到了自己左手边的餐叉,那么他们就都会开始等待
- 给每个叉子都赋予一个信号量,给每个信号量都用1初始化
-
一种方法:破除依赖
- 解决死锁问题的一个简单方法,就是修改某个或某些哲学家的拿餐叉顺序,Dijkstra自己也是这么解决这个问题的
- 假设4号哲学家的拿餐叉顺序与其他哲学家相反,是先拿右手的后拿左手的
/* getforks() 让编号为p的哲学家拿起左手的叉子,再拿起右手的叉子;编号为4就反过来 参数: p int类型,代表哲学家的编号 返回值: 无 */ void getforks(int p) { if (p == 4) { sem_wait(forks[right(p)]); // left(p): 哲学家p左手边叉子对应的信号量 sem_wait(forks[left(p)]); // right(p): 哲学家p右手边叉子对应的信号量 } else { sem_wait(forks[left(p)]); // left(p): 哲学家p左手边叉子对应的信号量 sem_wait(forks[right(p)]); // right(p): 哲学家p右手边叉子对应的信号量 } } - 这样就不会出现每个哲学家都拿着一个餐叉,然后卡住的情况
操作系统 - Operating System
/
Chapter III 并发
/
Section 1 线程
/
1-2 条件变量与信号量