TLPI: Ch30

18th March 2021 at 1:46pm
The Linux Programming Interface

PDF 及笔记:

电脑上如果 PDF 不展示或者展示不正常,使用 Chrome 并安装 PDF Viewer 插件。其他情况请下载文件查看:tlpi-ch30

部分笔记

Critical seciton(临界区)的定义:一段代码如果访问了线程间共享的资源、并且执行时需要是原子的(资源在其执行期间不被其他线程同时访问),这段 代码 则称为临界区。

即使像变量自增(glob++)这样简单的操作,也不能保证是原子的。比如在 RISC 精简指令集下,自增也需要被拆分成几个指令执行。

Mutex

Mutex 是 mutual exclusion 的意思,中文翻译为「互斥锁」:

  • 状态:locked, unlocked
  • 动作:
    • 加锁:lock / acquire
    • 解锁:unlock / release

规则

  1. 一个线程加锁后,就拥有这个锁,只有它能将其解锁
  2. 其他线程也尝试加这个锁时,会阻塞或者失败(视加锁方式而定)
  3. 锁是加在临界区(critical section)的,即是加在某段代码中,而这段代码中往往有访问共享资源的逻辑
  4. 但锁并不能强行保护共享资源。如果其他线程无视锁的存在,在访问共享资源前不先尝试加锁,那么它仍然可以访问该资源

使用 mutex 保护临界区

看 PDF 代码示例会加深理解。

有额外的加锁方式trylock()timedlock()

Mutex 的性能好,大部分情况下是在 user space 做操作,没有系统调用带来的消耗。

使用 mutex 可能带来死锁。最简单的场景:线程 A 加了锁 1,准备加锁 2;线程 B 加了锁 2,准备加锁 1。两者会无限期的互相阻塞。

解决方法:

  • 定义一套锁的层次结构,各线程加锁时都遵循一样的顺序
  • 各线程使用 trylock(),一旦无法加锁,就把之前加的锁全部释放,在一定时间间隔后再重试

有不同的 mutex 类型。它们针对这三种场景有不同的行为:

  • 同个线程对同个 mutex 加锁多次
  • 一个线程对一个它不拥有的锁做 unlock 操作
  • 一个线程对一个还没被上锁的锁做 unlock 操作

三种不同的 mutex type 有不同的行为:

  • NORMAL:没有死锁检测。线程加锁其已拥有的锁时,造成死锁。Unlock 其不拥有的锁或者未加锁的锁时,行为是未定义的(Linux 下不会报错)
  • ERROR_CHECK:上述三种场景都会报错。这种 mutex 性能比 NORMAL 稍差,但适合用来调试
  • RECURSIVE:一个线程不停对某个 mutex 加锁时,这种模式下系统会记录一下加锁计数。线程需要做跟其加锁次数一样多次的 unlock ,才能将一个 mutex 释放。对于其他两种场景,这种模式下会报错

SUSv3 规定默认的模式为 DEFAULT,但它对上述三种场景的行为故意没有定义。在 Linux 下,默认模式等同于 NORMAL。

Condition Variables

Condition variables 是一种线程间协作的机制。典型场景是:

  • 多个线程共同操作、关心某些共享变量,它们可以通过 condvar 来传递信号
  • 线程 1 修改完变量后,可以调用 pthread_cond_signal() 触发关心这些变量的其他进程醒来
  • 其他进程通过 pthread_cond_wait() 收到信号醒来后,检查共享变量并执行相应的逻辑

看代码示例会加深理解。代码写起来有一些较难理解的部分,我在 PDF 中加了注释。