锁的基本概念
基本概念
基本可锁定 (BasicLockable)
基本可锁定 是为执行代理(即线程)提供排他性阻塞语义的最小特征。
要求
对于 基本可锁定 的类型 L , L 类型对象 m 必须满足下列条件:
| 表达式 | 前置条件 | 效果 |
|---|---|---|
| m.lock() | 阻塞到能为当前执行代理(线程、进程、任务)取得锁为止。若抛异常,则不取得锁。 | |
| m.unlock() | 当前执行代理在 m 上保有非共享锁。 | 释放执行代理曾保有的 非共享锁 。不抛异常。 |
非共享锁
若对象上的一个锁由调用 lock、 try_lock、 try_lock_for 或 try_lock_until 成员函数取得,则称之为 非共享锁 。
可锁定 (Lockable)
可锁定 扩展 基本可锁定 ,以支持有意图的锁定。
要求
对于 可锁定 的类型 L ,它必须满足 基本可锁定 要求还有以下的条件:
| 表达式 | 效果 | 返回值 |
|---|---|---|
| m.try_lock() | 试图为当前执行代理(线程、进程、任务)取得锁,而不阻塞。若抛异常,则不取得锁。 | 若取得锁则为 true ,否则为 false |
互斥体 (Mutex)
互斥体 扩展 可锁定 以支持线程间同步。
要求
- 可锁定 (Lockable)
- 默认构造 (DefaultConstructible)
- 可析构 (Destructible)
- 不可复制
- 不可移动
- 其他要求
满足 互斥体 (Mutex)的标准库类型有:
- std::mutex
- std::recursive_mutex
- std::timed_mutex
- std::recursive_timed_mutex
- std::shared_mutex
锁的本质
锁在计算机里本质上是一块内存空间。当这个空间被赋值为1的时候表示加锁,被赋值为0的时候表示解锁。由硬件来保证一次只有一个线程能抢到锁。
CAS(Compare and Swap-比较并替换)
CAS是一种硬件级别的原子操作(通过锁定系统总线或某一块cache line来实现的)。CAS是实现并发算法时常用到的一种技术,它的原型可以认为是:
bool CAS(V, A, B)
其中V代表内存中的变量,A代表期待的值,B表示新值。当V的值与A相等时,将V与B的值交换。逻辑上可以用下面的伪代码表示:
bool CAS(V, A, B)
{
if (V == A)
{
swap(V, B);
return true;
}
return false;
}
需要强调的是上面的操作是原子的,要么不做,要么全部完成。
自旋锁(spin lock)
自旋锁是一种基础的同步原语,用于保障对共享数据的互斥访问。
一个spin lock就是让当前线程不断在while里面循环进行compare-and-swap,直到前面的线程放手(对应的内存被赋值0)。这个过程不需要操作系统的介入,这是运行程序和硬件之间的故事。如果需要长时间的等待,这样反复CAS轮询就比较浪费资源,这个时候程序可以向操作系统申请被挂起,然后持锁的线程解锁了以后再通知它。这样CPU就可以用来做别的事情,而不是反复轮询。但是OS切换线程也需要一些开销(上下文的切换),所以是否选择被挂起,取决于是否需要等很长时间。
自旋锁的伪代码实现如下:
b = true;
while(!CAS(flag, false, b));
//do something
flag = false;
C++11的Atomic中的compare_exchange_weak 与 compare_exchange_strong是著名的CAS。将this的值与expeced比较,若相等,把desired赋值给this,返回true;若不能把desired赋值给expected,返回false。
mutex类
提供排他性非递归所有权语义,保护共享数据免受从多个线程同时访问的同步原语。
与spin lock不同的是,mutex抢不到锁会把当前线程休眠,让出CPU,等待锁的状态发生变化时再被唤醒。
lock_guard和unique_lock等等都是RAII风格机制的互斥体包装器。
推荐使用mutex,而不是读写锁,因为读写锁有性能问题。
条件变量( condition_variable)
mutex解决的是线程互斥问题。条件变量解决的是线程等待问题。
条件变量要和std::unique_lock<std::mutex>一起使用。因为条件变量会涉及多次解锁和加锁,unique_lock提供了lock和unlock能力。
带谓词版本的wait_for会轮询等待,不带谓词版本的wait_for不会轮询等待。
条件变量存在两种情形:
- 唤醒丢失:notify的线程先执行,wait_for的线程后执行,导致无法收到通知一直处于等待状态。
- 虚假唤醒:存在多个线程wait,发送方线程执行了notify,可能所有线程都被唤醒,但只有一个线程能竞争到锁,其余线程无事可做,好像是无缘无故地被唤醒看,称为虚假唤醒。
为什么通知线程必须在锁的保护下修改变量?
如果通知线程不持有锁,那么可能在unique_lock释放锁和条件变量wait休眠之间更改了变量值并调用了notify,导致条件变量唤醒丢失,陷入死锁。
unique_lock释放锁和条件变量wait休眠是一个原子操作,如果通知线程持有锁,那么就只能等待条件变量wait休眠之后才能修改变量,调用notify。

浙公网安备 33010602011771号