美文网首页iOS精华
深入理解各种锁

深入理解各种锁

作者: Fly晴天里Fly | 来源:发表于2016-12-24 16:14 被阅读918次

    1 临界区

    1.1简介

    在早期计算机系统中,只有一个任务进程在执行,并不存在资源的共享与竞争。随着技术和需求的飞速发展,单个CPU通过时间分片在一段时间内同时处理多个任务进程,当多个进程对共享资源进行并发访问,就引起了进程间的竞态。这其中包括了我们所熟知的SMP(对称多处理器结构)系统,多核间的相互竞争,单CPU中断和进程间的相互抢占等诸多问题。

    更具体的说,对某段代码而言,可能会在程序中多次被执行(多线程便是典型的场景),每次执行的过程我们称作代码的执行路径,当两个或多个代码路径要竞争共同的资源的时候,该代码段就是临界区如图1所示


    临界区的执行

    为了保护共享资源不被同时访问,Linux内核中提供了各式各样的同步锁机制,包括:原子操作、自旋锁、信号量、互斥量等等,除了原子操作无论哪种锁机制都并非是免费的午餐,加锁操作伴随着用户态到内核态切换、进程上下文切换等高消耗过程。

    1.2 用户态与内核态切换

    为了集中管理,减少有限资源的访问和使用冲突,CPU设置了多个特权级别,就Intel x86架构的CPU来说一共有0~3四个特权级,0级最高,3级最低,硬件上在执行每条指令时都会对指令所具有的特权级做相应的检查,相关的概念有 CPL、DPL和RPL,这里不再过多阐述。为了安全考虑,Linux系统分为内核态和用户态,分别运行在内核空间和用户空间,对应的使用了0级特权级和3级特权级。

    内核态的程序可以执行特权指令,操作系统本身也在其中运行。

    用户态则不允许直接访问操作系统的核心数据、设备等关键资源,必须先通过系统调用或者中断进入内核态才可以访问,当系统调用或中断返回时,重新回到用户空间运行。

    由用户态切换到内核态的步骤主要包括:
    1)从当前进程的描述符中提取其内核栈的ss0及esp0信息。
    2)使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
    3)将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。
    简单来说用户态与内核态切换一般都需要保存用户程序得上下文(context), 在进入内核得时候需要保存用户态得寄存器,在内核态返回用户态得时候会恢复这些寄存器得内容,相对而言,这是一个很大的开销。

    1.3 进程上下文切换

    上下文切换的定义,http://www.linfo.org/context_switch.html 此文中已做了详细的说明,只提炼以下几个关键要点:

    1)进程上下文切换可以描述为kernel执行下面的操作
    a. 挂起一个进程,并储存该进程当时寄存器和程序计数器的状态
    b. 从内存中恢复下一个要执行的进程,恢复该进程原来的状态到寄存器,返回到其上次暂停的执行代码然后继续执行
    2)上下文切换只能发生在内核态,所以还会触发用户态与内核态切换

    2. Linux锁机制

    2.1 自旋锁

    自旋锁的实现是为了保护一段短小的临界区操作代码,保证这个临界区的操作是原子的,从而避免并发的竞争。在Linux内核中,自旋锁通常用于包含内核数据结构的操作,你可以看到在许多内核数据结构中都嵌入有spinlock,这些大部分就是用于保证它自身被操作的原子性,在操作这样的结构体时都经历这样的过程:上锁-操作-解锁。如果内核控制路径发现自旋锁“开着”(可以获取),就获取锁并继续自己的执行。相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径“锁着”,就在原地“旋转”,反复执行一条紧凑的循环检测指令,直到锁被释放。 自旋锁是循环检测“忙等”,即等待时内核无事可做(除了浪费时间),进程在CPU上保持运行,所以它保护的临界区必须小,且操作过程必须短。不过,自旋锁通常非常方便,因为很多内核资源只锁极短的时间片段,所以等待自旋锁的释放不会消耗太多CPU的时间。

    2.2.1 自旋锁需要做的工作

    从保证临界区访问原子性的目的来考虑,自旋锁应该阻止在代码运行过程中出现的任何并发干扰。这些“干扰”包括:

    1. 中断,包括硬件中断和软件中断 (仅在中断代码可能访问临界区时需要) 这种干扰存在于任何系统中,一个中断的到来导致了中断例程的执行,如果在中断例程中访问了临界区,原子性就被打破了。所以如果在某种中断例程中存在访问某个临界区的代码,那么就必须用spinlock保护。对于不同的中断类型(硬件中断和软件中断)对应于不同版本的自旋锁实现,其中包含了中断禁用和开启的代码。但是如果你保证没有中断代码会访问临界区,那么使用不带中断禁用的自旋锁API即可。

    2. 内核抢占(仅存在于可抢占内核中) 在2.6以后的内核中,支持内核抢占,并且是可配置的。这使UP系统和SMP类似,会出现内核态下的并发。这种情况下进入临界区就需要避免因抢占造成的并发,所以解决的方法就是在加锁时禁用抢占(preempt_disable(); ),在开锁时开启抢占(preempt_enable();注意此时会执行一次抢占调度)

    3. 其他处理器对同一临界区的访问 (仅SMP系统) 在SMP系统中,多个物理处理器同时工作,导致可能有多个进程物理上的并发。这样就需要在内存加一个标志,每个需要进入临界区的代码都必须检查这个标志,看是否有进程已经在这个临界区中。这种情况下检查标志的代码也必须保证原子和快速,这就要求必须精细地实现,正常情况下每个构架都有自己的汇编实现方案,保证检查的原子性。

    根据上的介绍,我们很容易知道自旋锁的操作包括:

    中断控制(仅在中断代码可能访问临界区时需要)
    抢占控制(仅存在于可抢占内核中需要)
    自旋锁标志控制 (仅SMP系统需要)
    中断控制是按代码访问临界区的不同而在编程时选用不同的变体,有些API中有,有些没有。
    而抢占控制和自旋锁标志控制依据内核配置(是否支持内核抢占)和硬件平台(是否为SMP)的不同而在编译时确定。如果不需要,相应的控制代码就编译为空函数。 对于非抢占式内核,由自旋锁所保护的每个临界区都有禁止内核抢占的API,但是为空操作。由于UP系统不存在物理上的并行,所以可以阉割掉自旋的部分,剩下抢占和中断操作部分即可。

    有些人会以为自旋锁的自旋检测可以用for实现,这种想法“Too young, too simple, sometimes naive”!你可以在理论上用C去解释,但是如果用for,起码会有如下两个问题:
    1)你如何保证在SMP下其他处理器不会同时访问同一个的标志呢?(也就是标志的独占访问)
    2)必须保证每个处理器都不会去读取高速缓存而是真正的内存中的标志(可以实现,编程上可以用volitale)要根本解决这个问题,需要在芯片底层实现物理上的内存地址独占访问,并且在实现上使用特殊的汇编指令访问。请看参考资料中对于自旋锁的实现分析。以arm为例,从存在SMP的ARM构架指令集开始(V6、V7),采用LDREX和STREX指令实现真正的自旋等待。

    2.2.2 自旋锁变体的使用规则

    不论是抢占式UP、非抢占式UP还是SMP系统,只要在某类中断代码可能访问临界区,就需要控制中断,保证操作的原子性。所以这个和模块代码中临界区的访问还有关系,是否可能在中断中操作临界区,只有程序员才知道。所以自旋锁API中有针对不同中断类型的自旋锁变体:
    不会在任何中断例程中操作临界区

    static inline void spin_lock(spinlock_t *lock)
    
    static inline void spin_unlock(spinlock_t *lock)
    

    如果在软件中断中操作临界区:

    static inline void spin_lock_bh(spinlock_t *lock)
    static inline void spin_unlock_bh(spinlock_t *lock)
    

    bh代表bottom half,也就是中断中的底半部,因内核中断的底半部一般通过软件中断(tasklet等)来处理而得名。
    如果在硬件中断中操作临界区:

    static inline void spin_lock_irq(spinlock_t *lock)
    static inline void spin_unlock_irq(spinlock_t *lock)
    

    如果在控制硬件中断的时候需要同时保存中断状态:

    spin_lock_irqsave(lock, flags)
    static inline void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)
    

    这些情况描诉似乎有点简单,我在网上找到了一篇使用规则((转)自旋锁(spinlock) 解释得经典,透彻),非常详细。我稍作修改,转载如下:

    获得自旋锁和释放自旋锁有好几个版本,因此让读者知道在什么样的情况下使用什么版本的获得和释放锁的宏是非常必要的。

    如果被保护的共享资源只在进程上下文访问和软中断(包括tasklet、timer)上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断。但是使用spin_lock_bh和spin_unlock_bh是最恰当的,它比其他两个快。

    如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。

    如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此。实际上tasklet在调用tasklet_schedule标记其需要被调度时已经把该tasklet绑定到当前CPU,因此同一个tasklet决不可能同时在其他CPU上运行。timer也是在其被使用add_timer添加到timer队列中时已经被帮定到当前CPU,所以同一个timer绝不可能运行在其他CPU上。当然同一个tasklet有两个实例同时运行在同一个CPU就更不可能了。

    如果被保护的共享资源只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源需要用spin_lock和spin_unlock来保护,因为同样的软中断可以同时在不同的CPU上运行。

    如果被保护的共享资源在两个或多个软中断上下文访问,那么这个共享资源当然更需要用spin_lock和spin_unlock来保护,不同的软中断能够同时在不同的CPU上运行。

    如果被保护的共享资源在软中断(包括tasklet和timer)或进程上下文和硬中断上下文访问,那么在软中断或进程上下文访问期间,可能被硬中断打断,从而进入硬中断上下文对共享资源进行访问,因此,在进程或软中断上下文需要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

    而在中断处理句柄中使用什么版本,需依情况而定,如果只有一个中断处理句柄访问该共享资源,那么在中断处理句柄中仅需要spin_lock和spin_unlock来保护对共享资源的访问就可以了。因为在执行中断处理句柄期间,不可能被同一CPU上的软中断或进程打断。

    但是如果有不同的中断处理句柄访问该共享资源,那么需要在中断处理句柄中使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

    在使用spin_lock_irq和spin_unlock_irq的情况下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具体应该使用哪一个也需要依情况而定,如果可以确信在对共享资源访问前中断是使能的,那么使用spin_lock_irq更好一些。因为它比spin_lock_irqsave要快一些,但是如果你不能确定是否中断使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因为它将恢复访问共享资源前的中断标志而不是直接使能中断。

    当然,有些情况下需要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好。

    spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效和软中断失效却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问。

    2.2.3 自旋锁使用及注意事项

    自旋锁使用如下;

    //1.分配自旋锁
    spinlock_t lock;
    //2.初始化自旋锁
    spin_lock_init(&lock);
    //3.访问临界区之前获取锁:
    spin_lock(&lock);  //获取自旋锁,立即返回,如果没有获取锁,将进行忙等待
     或者
    spin_trylock(&lock); //获取锁,返回true,否则返回false,所以这个函数一定要对返回值进行判断!
    //4 .访问临界区
    //5.释放自旋锁
    spin_unlock(&lock);
    

    自旋锁的注意事项:

    1. 自旋锁使CPU处于忙等状态,因此临界区执行时间应该尽量短;

    2. 自旋锁是不可重入的;

    3. 自旋锁保护的临界区不应该有睡眠操作:

      1)对于开中断的自旋锁来说,睡眠操作可能发生如下两种情况:
      a. 死锁:任务A获得自旋锁之后睡眠,接着又发生了中断,而中断处理程序内部又打算获取同一个自旋锁,则此时会发生自死锁 —— 自旋锁是不可重入的。
      b. CPU浪费:倘若中断处理程序内部没有获取同一个自旋锁的操作,则理论上可以产生调度。假设进程B打算获取CPU的控制权,但由于此时是关抢占的(因为进程A还没有解自旋锁,此时依旧处于自旋锁的临界区中),导致进程B无法运行。也就是说CPU将无法运行任何程序,一直处于无事可做的状态,造成CPU的浪费。

      2)对于顺带关中断的自旋锁来说,显而易见在临界区内使不能睡眠的,因为唤醒一个睡眠的进程依赖于调度器,而调度器是通过时钟中断来判断合适唤醒进程的,倘若在关闭中断的时候进程睡眠,则调度器将再也无法收到时钟中断(因为开中断的操作也是由该进程控制的),从而永远都无法唤醒睡眠的进程。也就是说该进程将处于睡死状态。

    简单来说,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。

    2.3 信号量

    信号量是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。一般说来,为了获得共享资源,进程需要执行下列操作:
    1) 测试控制该资源的信号量。   
    2) 若此信号量的值为正,则允许进行使用该资源。进程将信号量减1。   
    3) 若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1)。   
    4) 当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。

    维护信号量状态的是Linux内核操作系统而不是用户进程。我们可以从头文件/usr/src/linux/include/linux/sem.h 中看到内核用来维护信号量状态的各个结构的定义。信号量是一个数据集合,用户可以单独使用这一集合的每个元素。要调用的第一个函数是semget,用以获得一个信号量ID。Linux2.6.26下定义的信号量结构体:

    struct semaphore {
        spinlock_t lock;
        unsigned int count;
        struct list_head wait_list;
    };
    

    从以上信号量的定义中,可以看到信号量底层使用到了spin lock的锁定机制,这个spinlock主要用来确保对count成员的原子性的操作(count–)和测试(count > 0)。

    2.3.1 信号量的P操作

    1. void down(struct semaphore *sem);
    2. int down_interruptible(struct semaphore *sem);
    3. int down_trylock(struct semaphore *sem);

    函数1表示当信号申请不到时会进程会休眠;对于函数(2)来说,它表示如果当进程因申请不到信号量而进入睡眠后,能被信号打断,这里所说的信号是指进程间通信的信号,比如我们的Ctrl+C,但这时候这个函数的返回值不为0;

    int down_interruptible(struct semaphore *sem)
    {
            unsigned long flags;
            int result = 0;
      
            spin_lock_irqsave(&sem->lock, flags);
            if (likely(sem->count > 0))
                    sem->count--;
            else
                    result = __down_interruptible(sem);
            spin_unlock_irqrestore(&sem->lock, flags);
      
            return result;
    }
    

    对此函数的理解:在保证原子操作的前提下,先测试count是否大于0,如果是说明可以获得信号量,这种情况下需要先将count--,以确保别的进程能否获得该信号量,然后函数返回,其调用者开始进入临界区。如果没有获得信号量,当前进程利用struct semaphore 中wait_list加入等待队列,开始睡眠。

    对于需要休眠的情况,在__down_interruptible()函数中,会构造一个struct semaphore_waiter类型的变量(struct semaphore_waiter定义如下:

    struct semaphore_waiter
    {
        struct list_head list;
        struct task_struct *task;
        int up;
    };
    

    将当前进程赋给task,并利用其list成员将该变量的节点加入到以sem中的wait_list为头部的一个列表中,假设有多个进程在sem上调用down_interruptible,则sem的wait_list上形成的队列如下图:

    等待队列示意图

    (注:将一个进程阻塞,一般的经过是先把进程放到等待队列中,接着改变进程的状态,比如设为TASK_INTERRUPTIBLE,然后调用调度函数schedule(),后者将会把当前进程从cpu的运行队列中摘下)
    函数(3)试图去获得一个信号量,如果没有获得,函数立刻返回1而不会让当前进程进入睡眠状态。

    2.3.2 信号量的V操作

    void up(struct semaphore *sem);
    原型如下:

    void up(struct semaphore *sem)
    {
        unsigned long flags;
        spin_lock_irqsave(&sem->lock, flags);
        if (likely(list_empty(&sem->wait_list)))
                sem->count++;
        else
                __up(sem);
        spin_unlock_irqrestore(&sem->lock, flags);
    }
    

    如果没有其他线程等待在目前即将释放的信号量上,那么只需将count++即可。如果有其他线程正因为等待该信号量而睡眠,那么调用__up.
    __up的定义:

    static noinline void __sched __up(struct semaphore *sem)
    {
        struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list);
        list_del(&waiter->list);
        waiter->up = 1;
        wake_up_process(waiter->task);
    }
    

    这个函数首先获得sem所在的wait_list为头部的链表的第一个有效节点,然后从链表中将其删除,然后唤醒该节点上睡眠的进程。 由此可见,对于sem上的每次down_interruptible调用,都会在sem的wait_list链表尾部加入一新的节点。对于sem上的每次up调用,都会删除掉wait_list链表中的第一个有效节点,并唤醒睡眠在该节点上的进程。

    2.3.3 信号量的使用

    //1.分配信号量对象
      struct semaphore sema;
    //2.初始化为互斥信号量
      init_MUTEX(&sema);
    或者:
      DECLARE_MUTEX(sema);
    //3.访问临界区之前获取信号量
      down(&sema);
      //如果获取信号量,立即返回
      //如果信号量不可用,进程将在此休眠,并且休眠的状态是 [ 不可中断的休眠状态 TASK_UNINTERRUPTIBLE] !
      或者
      down_interruptible(&sema);
      //如果信号量不可用,进程将进入 [ 可中断的休眠状态 TASK_INTERRUPTIBLE ],如果返回0表示正常获取信号,如果返回非0,表示接受到了信号
      down_trylock();
      //获取信号,如果信号量不可用,返回非0,如果信号量可用,返回0;不会引起休眠,可以在中断上下文使用。返回值也要做判断!
    //4.访问临界区:临界区可以休眠
    //5.释放信号量
      up(&sema);
      //不仅仅释放信号量,然后唤醒休眠的进程,让这个进程去获取信号量来访问临界区
    

    2.4 互斥量

    互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex))。互斥体禁止多个线程同时进入受保护的代码“临界区”(critical section)。因此,在任意时刻,只有一个线程被允许进入这样的代码保护区。任何线程在进入临界区之前,必须获取(acquire)与此区域相关联的互斥体的所有权。如果已有另一线程拥有了临界区的互斥体,其他线程就不能再进入其中。这些线程必须等待,直到当前的属主线程释放(release)该互斥体。

    Linux 2.6.26中mutex的定义:

    struct mutex {
            /* 1: unlocked, 0: locked, negative: locked, possible waiters */
            atomic_t                  count;
            spinlock_t                wait_lock;
            struct list_head          wait_list;
    #ifdef CONFIG_DEBUG_MUTEXES
            struct thread_info        *owner;
            const char                *name;
            void                      *magic;
    #endif
    #ifdef CONFIG_DEBUG_LOCK_ALLOC
            struct lockdep_map         dep_map;
    #endif
    };
    

    对比前面的struct semaphore,struct mutex除了增加了几个作为debug用途的成员变量外,和semaphore几乎长得一样。但是mutex的引入主要是为了提供互斥机制,以避免多个进程同时在一个临界区中运行。

    如果静态声明一个count=1的semaphore变量,可以使用DECLARE_MUTEX(name),DECLARE_MUTEX(name)实际上是定义一个semaphore,所以它的使用应该对应信号量的P,V函数.

    如果要定义一个静态mutex型变量,应该使用DEFINE_MUTEX

    如果在程序运行期要初始化一个mutex变量,可以使用mutex_init(mutex),mutex_init是个宏,在该宏定义的内部,会调用__mutex_init函数。

    #define mutex_init(mutex) \
    do { \
        static struct lock_class_key __key; \
        \
        __mutex_init((mutex), #mutex, &__key); \
    } while (0)
      
    void __mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key)
    {
        atomic_set(&lock->count, 1);
        spin_lock_init(&lock->wait_lock);
        INIT_LIST_HEAD(&lock->wait_list);
        debug_mutex_init(lock, name, key);
      
    }
    

    从__mutex_init的定义可以看出,在使用mutex_init宏来初始化一个mutex变量时,应该使用mutex的指针型。
    mutex上的P,V操作:void mutex_lock(struct mutex *lock)和void __sched mutex_unlock(struct mutex *lock)

    从原理上讲,mutex实际上是count=1情况下的semaphore,所以其PV操作应该和semaphore是一样的。但是在实际的Linux代码上,出于性能优化的角度,并非只是单纯的重用down_interruptible和up的代码。以ARM平台的mutex_lock为例,实际上是将mutex_lock分成两部分实现:fast path和slow path,主要是基于这样一个事实:在绝大多数情况下,试图获得互斥体的代码总是可以成功获得。所以Linux的代码针对这一事实用ARM V6上的LDREX和STREX指令来实现fast path以期获得最佳的执行性能。
    mutux底层支持:

    Linux:底层的pthread mutex采用futex(2)(fast userspace mutex)实现,不必每次加锁、解锁都陷入系统调用(从用户态切换到内核态)。

    futex(2):快速用户态互斥锁(fast userspace mutex),在非竞态(或非锁争用,表示申请即能立即拿到锁而不用等待)的情况下,futex操作完全在用户态下进行,内核态只负责处理竞态(或锁争用,表示申请了但有其它线程提前拿到了锁,需要等待锁被释放后才能拿到)下的操作(调用相应的系统调用来仲裁),futex有两个主要的函数:

    futex_wait(addr, val)   //检查*addr == val ?,若相等则将当前线程放入等待队列addr中随眠(陷入到内核态等待唤醒),否则调用失败并返回错误(依旧处于用户态)
    futex_wake(addr, val)   //唤醒val个处于等待队列addr中的线程(从内核态回到用户态
    

    因此采用futex(2)的互斥锁不必每次加解锁都从用户态切换到内核态,效率较高.
    Windows:底层的CRITICAL_SECTION嵌入了一小段自旋锁,如果不能立即拿到锁,它先会自旋一小段时间,如果还拿不到,才挂起当前线程.

    2.4.1 互斥量的使用

    pthread mutex接口函数:

    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
        
    int pthread_mutex_lock(pthread_mutex_t *mutex);
        
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
        
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    

    3. 各种锁的区别

    3.1 信号量/互斥体和自旋锁的区别

    信号量/互斥体允许进程睡眠属于睡眠锁,自旋锁则不允许调用者睡眠,而是让其循环等待,所以有以下区别应用 :

    1. 信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因而自旋锁适合于保持时间非常短的情况
    2. 自旋锁可以用于中断,不能用于进程上下文(会引起死锁)。而信号量不允许使用在中断中,而可以用于进程上下文
    3. 自旋锁保持期间是抢占失效的,自旋锁被持有时,内核不能被抢占,而信号量和读写信号量保持期间是可以被抢占的

    另外需要注意的是:

    1. 信号量锁保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区,因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一进程企图获取本自旋锁,死锁就会发生。
    2. 在你占用信号量的同时不能占用自旋锁,因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。

    3.2 信号量和互斥体之间的区别

    概念上的区别:
    信号量:是进程间(线程间)同步用的,一个进程(线程)完成了某一个动作就通过信号量告诉别的进程(线程),别的进程(线程)再进行某些动作。有二值和多值信号量之分。
    互斥锁:是线程间互斥用的,一个线程占用了某一个共享资源,那么别的线程就无法访问,直到这个线程离开,其他的线程才开始可以使用这个共享资源。可以把互斥锁看成二值信号量。

    上锁时:
    信号量: 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait阻塞,直到sem_post释放后value值加一。
    互斥锁: 只要被锁住,其他任何线程都不可以访问被保护的资源。如果没有锁,获得资源成功,否则进行阻塞等待资源可用。
    使用场所: 信号量主要适用于进程间通信,当然,也可用于线程间通信。而互斥锁只能用于线程间通信。

    相关文章

      网友评论

        本文标题:深入理解各种锁

        本文链接:https://www.haomeiwen.com/subject/xmcwmttx.html