信号量 vs 互斥锁

作于: 2021 年 8 月 26 日,预计阅读时间 7 分钟

go-patterns/semaphore.md at master · tmrts/go-patterns (github.com) 时产生了疑问,信号量为啥长得和互斥锁没啥区别呢。于是就谷歌了一圈,重温下一些关于并发的知识,对比信号量 semaphore 和互斥锁 mutex

互斥锁 mutex

pthread 自带的互斥锁为例,提供了三种不同类型的互斥锁:

可以比较清楚地看出,互斥锁有三个基本特征:

最严格的 PTHREAD_MUTEX_ERRORCHECK 类型互斥锁,对此定义是 NO、NO、NO 。

互斥锁的基本使用方式和使用场景有点像厕所的坑位:

  1. 抢坑位,锁门
  2. 你懂的
  3. 解锁,出门

其中有隐含的信息包括:

  1. 坑位是提前选择好的,你只能抢一个坑位,不能抢多个坑位。
  2. 坑位在使用期间是独占的,你不能和别人分享一个坑位。
  3. 只有你自己能解锁坑位,谁也不想办事儿的时候有人闯进来吧?

而递归加锁这一特殊场景,我寻思吧,有点难拿坑位比喻。反正也不重要,就别管了。

信号量 semaphore

信号量本质上是一个整型值,不细分什么类型了。还是用 pthread 举例吧,依据 POSIX 标准。

对信号量的操作可以先简单分5种。

信号量的主要特征就是它的值:

可以注意到,信号量的确可以做到互斥锁能做到的事情:设定好初始值1,然后sem_wait等同于加锁,sem_post等同于解锁,的确模拟出了互斥锁的功能。

不过信号量去模拟互斥锁会有一些问题。比如说无法实现递归加锁(信号量值等于0时,sem_wait会阻塞),无法检测解锁线程是不是加锁线程(除非你自己再封装一次,把信号量和线程ID绑定),解锁未加锁会导致信号量值大于1,进而造成sem_wait会允许多个线程并行执行(还是一样,你得自己封装,在sem_post前检查当前信号量的值)。

好,模拟互斥锁的话题到此为止。回到屎尿屁的比喻上。互斥锁可以比作公厕收费的老大爷。

其中隐含的信息包括:

信号量最好用的场景还是 生产者-消费者 模型的队列,来统计队列中元素数量。消费者可以用一个简单的 sem_timedwait 调用实现等待新元素加入队列,用互斥锁来确保队列操作是线程安全的。

可见管公厕的老大爷也是非常有生活智慧哈,充分利用了年轻时的编程经验来提高晚年生活质量。

结论

互斥锁和信号量都能处理数据竞争,但各有侧重。

典型的数据竞争场景当然是互斥锁好用,但信号量也不是完全不行。

信号量的典型场景也一样,互斥锁即便能行也会显得别扭。

/golang/