《Linux多线程服务端编程》笔记——线程同步精要

时间:2023-03-09 09:47:26
《Linux多线程服务端编程》笔记——线程同步精要

并发编程基本模型
message passing和shared memory。

线程同步的四项原则

  • 尽量最低限度地共享对象,减少需要同步的场合。如果确实需要,优先考虑共享 immutable 对象。
  • 使用高级的并发编程构件,如TaskQueue、Producer-Consumer Queue、CountDownLatch等等。
  • 不得已必须使用底层同步原语(primitives)时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量。
  • 除了使用 atomic 整数之外,不自己编写 lock-free 代码,也不要用“内核级”同步原语。不凭空猜测“哪种做法性能会更好”,比如 spin lock vs. mutex。

互斥器的使用

  • 用 RAII 手法封装 mutex 的创建、销毁、加锁、解锁这四个操作。保证锁的生效期间等于一个作用域(scope)。
  • 只用非递归的 mutex(即不可重入的 mutex)。
  • 不手工调用 lock() 和 unlock() 函数,一切交给栈上的 Guard 对象的构造和析构函数负责(Scoped Locking)。
  • 在每次构造 Guard 对象的时候,思考一路上(调用栈上)已经持有的锁,防止因加锁顺序不同而导致死锁。

条件变量的使用

  • 对于 wait() 端:
    必须与 mutex 一起使用,该布尔表达式的读写需受此 mutex 的保护。
    在 mutex 已上锁的情况下才能调用 wait()。
    把判断布尔表达式和 wait() 放在 while 循环中。

  • 对于 signal/broadcast 端:
    不一定要在 mutex 已上锁的情况下调用 signal(理论上)。
    在 signal 之前一般要修改布尔表达式。
    修改布尔表达式通常需要用 mutex 保护(至少用作 full memory barrier)。
    broadcast 通常用于表明状态变化,signal 通常用于表示资源可用。

  • 虚假唤醒(spurious wakeup),Linux 中 futex 慢速系统调用被信号打断返回 -1,wait 返回了。

读写锁与信号量的使用

  • 从正确性方面来说,一种典型的易犯的错误是在持有 reader lock 的时候修改了共享数据。
  • 从性能方面来说,读写锁不见得比普通 mutex 更高效。
  • reader lock 可能允许提升为 writer lock,也可能不允许提升。
  • 通常 reader lock 是可重入的,writer lock 是不可重入的。
  • 信号量不是必备的同步原语,因为条件变量配合互斥器可以完全替代其功能,而且更不易用错。

参考:《Linux多线程服务端编程》。