在 Linux 多线程编程中,我们常常用 pthread_mutex_lock 来做线程间同步。当锁被占用时,当前线程会进入等待队列,直到锁被释放时才被唤醒。那么如果有多个线程同时在等待队列中,唤醒顺序是怎样的呢?文档中关于这点的解释比较简略:
If there are threads blocked on the mutex object referenced by mutex when pthread_mutex_unlock() is called, resulting in the mutex becoming available, the scheduling policy shall determine which thread shall acquire the mutex.
这里的 scheduling policy 是指操作系统的调度策略呢,还是指当前 mutex lock 类型的调度策略,上下文并没有提及。网上看到不少说法是不保证唤醒顺序,但有同事说他之前看过,PTHREAD_MUTEX_TIMED_NP 类型的锁是能保证先进先出顺序的。
我们先来看下 PTHREAD_MUTEX_TIMED_NP 的语义,libc 文档中解释了 posix mutex 的几种不同类型:
- PTHREAD_MUTEX_TIMED_NP 是默认类型,能用于 pthread_mutex_timedlock 调用,可以重复拿锁
- PTHREAD_MUTEX_ADAPTIVE_NP 会先尝试做若干次 spin lock 再进入等待队列,不能重复拿锁
- PTHREAD_MUTEX_ERRORCHECK_NP 重复拿锁会报错
- PTHREAD_MUTEX_RECURSIVE_NP 可以重复拿锁,拿锁次数需要等于释放次数
所以 PTHREAD_MUTEX_TIMED_NP 这里的“timed”是指可以用于有超时限制地拿锁,而不是按照时间顺序唤醒的意思。我们可以进一步从 glibc 代码中确认,对于默认的 TIMED 类型,pthread_mutex_lock 直接调用底层锁,而后者用 Linux kernel 的 futex 实现锁的功能,并没有额外的队列排序功能。每次释放锁,都会调用 futex_wake syscall 尝试唤醒一个线程。
而 Linux futex lock 的实现是以锁的地址作为 key,把当前线程插入到对应的队列中。队列按照线程优先级排序,相同优先级的线程按照先进先出顺序排序。所以虽然 futex 语义和文档都明确提到不保证唤醒顺序,但至少目前的实现中相同优先级是 FIFO 顺序。为了验证这点,我修改了 p 哥之前的一段实验代码,加入优先级设置,并同时测试 mutex 和 futex。结果与前文所述一致。(一开始设置优先级总是不生效,折腾了很久之后才知道原来 pthread 默认是继承父线程的调度属性,必须设置成 PTHREAD_EXPLICIT_SCHED 模式才能改变调度策略和优先级。)
LWN 上有篇文章对 futex 作了简要介绍,可以快速了解 futex 实现。里面也提到了 futex 的一系列优化。比如最开始 futex 实现涉及到页表操作,需要拿 mmap_sem 锁;到 2008 年 9 月,futex 已经彻底消除了对 mmap_sem 的依赖,这样再也无需与 page fault 或 mmap 操作竞争抢锁。