线程同步方法 | 计算机基础

本文介绍了线程间同步的方法,包括锁、条件变量、信号量。

自旋锁

1
while (抢锁(lock) == 没抢到) {}  // 只要没有锁上,就不断重试

互斥锁

1
2
3
while (抢锁(lock) == 没抢到) {
本线程先去睡了请在这把锁的状态发生改变时再唤醒(lock);
}

操作系统负责线程调度,为了实现「锁的状态发生改变时再唤醒」就需要把锁也交给操作系统管理。所以互斥锁的加锁操作通常都需要涉及到上下文切换,操作花销也就会比自旋锁要大。

  • 可递归锁/可重入锁:同一个线程可以多次获取同一个锁,不会产生死锁
  • 非递归锁/不可重入锁:如果一个线程多次获取同一个锁,则会产生死锁

读写锁

在执行加锁操作时需要额外表明读写意图,读者之间并不互斥,而写者则要求与任何人互斥。

条件变量(条件锁)

基本操作:

  • 挂起线程等待其他线程触发条件
  • 触发条件

条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程,这些线程将重新锁定互斥锁并重新测试条件是否满足。

信号量

由于信号量只能进行两种操作等待和发送信号,即 P(sv) 和 V(sv),他们的行为是这样的:

  • P(sv):如果 sv 的值大于零,就给它减 1;如果它的值为零,就挂起该进程的执行
  • V(sv):如果有其他进程因等待 sv 而被挂起,就让它恢复运行,如果没有进程因等待 sv 而挂起,就给它加 1

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv) 操作,它将得到信号量,并可以进入临界区,使 sv 减 1。而第二个进程将被阻止进入临界区,因为当它试图执行 P(sv) 时,sv 为 0,它会被挂起以等待第一个进程离开临界区域并执行V(sv) 释放信号量,这时第二个进程就可以恢复执行。

信号量和互斥锁的区别:互斥锁只允许一个线程进入临界区,而信号量允许多个线程同时进入临界区。

附录:CAS(compare and swap)实现自旋锁

CAS 的原型可以认为是:

1
bool CAS(value, expect, new_value)

其中 value 代表内存中的变量,expect 代表期待的值,new_value 表示新值。当 value 的值与 expect 相等时,将 value 与 new_value 的值交换。

使用 CAS 可以实现自旋锁的逻辑:

1
2
3
4
b = true;
while(!CAS(flag, false, b)); // 获取锁
// 临界区
flag = false; // 释放锁

使用 C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SpinLock {
public:
SpinLock() : flag_(false) {}

void lock() {
bool expect = false;
while (!flag_.compare_exchange_weak(expect, true)) {
expect = false; // 这里要将 expect 复原,compare_exchange_weak 执行失败时 expect 结果是未定义的
}
}

void unlock() { flag_.store(false); }

private:
std::atomic<bool> flag_;
};
1
2
3
4
SpinLock my_lock;
my_lock.lock();
// 临界区
my_lock.unlock();

参考

linux c 线程间同步(通信)的几种方法–互斥锁,条件变量,信号量,读写锁

《后台开发:核心技术与应用实践》

如何理解互斥锁、条件锁、读写锁以及自旋锁?

使用C++11原子量实现自旋锁

线程同步方法 | 计算机基础

http://www.zh0ngtian.tech/posts/727aa8bd.html

作者

zhongtian

发布于

2019-07-28

更新于

2023-12-16

许可协议

评论