📂 操作系统 - Operating System / Chapter III 并发 / Section 1 线程 / 1-2 条件变量与信号量

信号量

2026-05-28
#OS
  • 信号量的定义

    • 信号量(semaphore) 是一个整数值的对象
    • 信号量的初始值可以决定其行为,所以需要在使用信号量前为其赋初值
    • 有两个函数可以用于操作信号量,分别是sem_wait()sem_post()
      • sem_wait():
        1. 先将信号量的值-1
        2. 然后如果信号量的值<0,那么线程开始等待
      • sem_post():
        1. 先将信号量的值+1
        2. 如果当前存在线程正在该信号量上等待,那么唤醒其中一个
    • 信号量的值为负数(-n) 时,就代表存在n个线程正在等待
  • 信号量用作锁(二值信号量)

    • 信号量作为锁使用时,我们需要给信号量赋初值为1
    • 假设有2个线程t0、t1,使用信号量s保护某个临界区
      1. t0即将进入临界区,调用sem_wait(),传入信号量s
      2. s的值-1,变为0t0正常进入临界区
      3. 此时若t1想进入临界区,就也需要调用sem_wait(),但接下来s的值变为-1,为负数,t1无法进入临界区,开始睡眠
      4. t0离开临界区,调用sem_post()s的值+1变为0,同时唤醒一条等待着的线程
      5. t1被唤醒,得以进入临界区
    • 在这种情况下,由于信号量只有两个状态持有没被持有,所以被称为二值信号量(binary semaphore)
  • 信号量用作条件变量

    • 信号量也可以用于让一个线程停止执行,等待另一条件成立,也就是用作条件变量
    • 假设现在有进程中初始有一个线程,该线程(父线程)会创建一个子线程,然后等待子线程运行结束,子线程结束后父线程才结束,此时我们需要给信号量赋初值为0
    • 父线程运行

      1. 父线程**创建子线程
      2. 子线程开始运行
      3. 父线程调用sem_wait()
      4. 父线程从sem_wait()中醒来后,父线程结束
    • 子线程运行

      1. 子线程开始
      2. 子线程调用sem_post(),并立即结束
    • 两种运行情况:

      • 第一种:父线程调用sem_wait()时,子线程还没有运行完毕
        1. 父线程创建子线程
        2. 子线程开始运行
        3. 父线程调用sem_wait(),此时信号量的值从0变为-1,为负数父线程开始等待
        4. 子线程运行到sem_post(),将信号量的值从-1变为0,并唤醒正在等待的父线程,紧接着子线程结束
        5. 父线程被唤醒,然后父线程结束
      • 第二种:父线程调用sem_wait()时,子线程已经运行完毕
        1. 父线程创建子线程
        2. 子线程开始运行
        3. 子线程很快就执行完毕了,调用sem_post(),将信号量的值从0变为1,并尝试唤醒正在等待的线程,此时没有线程正在等待,紧接着子线程结束
        4. 父线程调用sem_wait(),此时信号量的值从1变为0,不是负数父线程不用等待,直接继续执行
        5. 父线程结束
  • 读者-写者锁

    • 读者-写者锁(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右手边叉子对应的信号量
        }
        
      • 这种方案会有死锁问题,当每个哲学家都拿到了自己左手边的餐叉,那么他们就都会开始等待
    • 一种方法:破除依赖

      • 解决死锁问题的一个简单方法,就是修改某个或某些哲学家的拿餐叉顺序,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右手边叉子对应的信号量
        	}
        }
        
      • 这样就不会出现每个哲学家都拿着一个餐叉,然后卡住的情况