线程同步之自旋锁--SpinLock


什么是自旋锁

自旋锁(Spinlock)是一种广泛运用的底层同步机制。自旋锁是一个互斥设备,它只有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的某个位。希望获得某个特定锁得代码测试相关的位。如果锁可用,则“锁定”被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环(而不是休眠,这也是自旋锁和一般锁的区别)并重复检查这个锁,直到该锁可用为止,这就是自旋的过程。“测试并设置位”的操作必须是原子的,这样,即使多个线程在给定时间自旋,也只有一个线程可获得该锁。

自旋锁对于SMP和单处理器可抢占内核都适用。可以想象,当一个处理器处于自旋状态时,它做不了任何有用的工作,因此自旋锁对于单处理器不可抢占内核没有意义,实际上,非抢占式的单处理器系统上自旋锁被实现为空操作,不做任何事情。

曾经有个经典的例子来比喻自旋锁:A,B两个人合租一套房子,共用一个厕所,那么这个厕所就是共享资源,且在任一时刻最多只能有一个人在使用。当厕所闲置时,谁来了都可以使用,当A使用时,就会关上厕所门,而B也要使用,但是急啊,就得在门外焦急地等待,急得团团转,是为“自旋”,这也是要求锁的持有时间尽量短的原因!

自旋锁有以下特点:

  • 用于临界区互斥
  • 在任何时刻最多只能有一个执行单元获得锁
  • 要求持有锁的处理器所占用的时间尽可能短
  • 等待锁的线程进入忙循环

临界区和互斥:对于某些全局资源,多个并发执行的线程在访问这些资源时,操作系统可能会交错执行多个并发线程的访问指令,一个错误的指令顺序可能会导致最终的结果错误。多个线程对共享的资源的访问指令构成了一个临界区(critical section),这个临界区不应该和其他线程的交替执行,确保每个线程执行临界区时能对临界区里的共享资源互斥的访问。



自旋锁较互斥锁之类同步机制的优势

休眠与忙循环

互斥锁得不到锁时,线程会进入休眠,这类同步机制都有一个共性就是 一旦资源被占用都会产生任务切换,任务切换涉及很多东西的(保存原来的上下文,按调度算法选择新的任务,恢复新任务的上下文,还有就是要修改cr3寄存器会导致cache失效)这些都是需要大量时间的,因此用互斥之类来同步一旦涉及到阻塞代价是十分昂贵的。

一个互斥锁来控制2行代码的原子操作,这个时候一个CPU正在执行这个代码,另一个CPU也要进入, 另一个CPU就会产生任务切换。为了短短的两行代码 就进行任务切换执行大量的代码,对系统性能不利,另一个CPU还不如直接有条件的死循环,等待那个CPU把那两行代码执行完。

自旋过程

当锁被其他线程占有时,获取锁的线程便会进入自旋,不断检测自旋锁的状态。一旦自旋锁被释放,线程便结束自旋,得到自旋锁的线程便可以执行临界区的代码。对于临界区的代码必须短小,否则其他线程会一直受到阻塞,这也是要求锁的持有时间尽量短的原因!



自旋锁和互斥锁的区别

从实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。

  • 假设线程A想要通过 pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中, 此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。
  • 而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。

对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用 时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。

因此自旋锁和互斥锁适用于不同的场景

  • 自旋锁适用于那些仅需要阻塞很短时间的场景
  • 互斥锁适用于那些可能会阻塞很长时间的场景


使用C++实现自主实现自旋锁

#pragma once
 
#include <atomic>
 
class spin_lock {
private:
    std::atomic<bool> flag = ATOMIC_VAR_INIT(false);
public:
    spin_lock() = default;
    spin_lock(const spin_lock&) = delete;
    spin_lock& operator=(const spin_lock) = delete;
    void lock(){   //acquire spin lock
        bool expected = false;
        while(!flag.compare_exchange_strong(expected, true));
            expected = false;    
    }   
    void unlock(){   //release spin lock
        flag.store(false);
    }   
};

测试文件

int num = 0;
spin_lock sm; 
 
void thread_proc()
{
    for(int i=0; i<10000000; ++i){
        sm.lock();
        ++num;
        sm.unlock();
    }   
}


自旋锁与互斥锁的对比、C++11实现自旋锁
【原创+整理】线程同步之详解自旋锁
Linux应用 线程同步之自旋锁

posted @ 2024-07-10 13:38  guanyubo  阅读(18)  评论(0编辑  收藏  举报