锁的基本概念

基本概念

基本可锁定 (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不会轮询等待。
条件变量存在两种情形:

  1. 唤醒丢失:notify的线程先执行,wait_for的线程后执行,导致无法收到通知一直处于等待状态。
  2. 虚假唤醒:存在多个线程wait,发送方线程执行了notify,可能所有线程都被唤醒,但只有一个线程能竞争到锁,其余线程无事可做,好像是无缘无故地被唤醒看,称为虚假唤醒。

为什么通知线程必须在锁的保护下修改变量?
如果通知线程不持有锁,那么可能在unique_lock释放锁和条件变量wait休眠之间更改了变量值并调用了notify,导致条件变量唤醒丢失,陷入死锁。
unique_lock释放锁和条件变量wait休眠是一个原子操作,如果通知线程持有锁,那么就只能等待条件变量wait休眠之后才能修改变量,调用notify。

参考

mutex的底层原理是什么
使用C++原子量实现自旋锁
条件变量的使用及陷阱

posted @ 2023-05-06 00:07  _溯源  阅读(109)  评论(0)    收藏  举报