多线程锁的分析

本文深入探讨了多线程锁的各个方面,从原子操作如CAS,到内核中的RCU,再到各种类型的锁如互斥锁、spin_lock和递归锁。详细阐述了volatile的作用,并分析了死锁的现象、预防、避免和消除策略。内容覆盖了锁的底层实现原理,以及在多核系统中的优化技巧。
摘要由CSDN通过智能技术生成

多线程锁的分析

首先我们知道锁主要有两种,悲观锁和乐观锁。对于悲观锁(mutex,spin_lock),它永远会假定最糟糕的情况,就像我们上面说到的互斥机制,每次我们都假定会有其他的线程和我们竞争资源,因此必须要先拿到锁,之后才放心的进行我们的操作,这就使得争夺锁成为了我们每次操作的第一步。乐观锁(CAS,原子操作)则不同,乐观锁假定在很多情况下,资源都不需要竞争,因此可以直接进行读写,但是如果碰巧出现了多线程同时操控数据的情况,那么就多试几次,直到成功(也可以设置重试的次数)

底层调用的是cpu提供的汇编指令,这个可以认为是cpu设计时候提供的。

原子操作:

内联汇编

int fetch_addd(int *value, int add)
{
   
    __asm__ volatile (
    "lock; xaddl %2, %1;"//asm把cpu锁住,执行一句命令
        :"=a"(old)//output
        :"m"(*value),"a"(add)//input
        :"cc","memory"
    ); 
    return old;
}

原子操作对于我们来说,是非常熟悉的概念。在某些场景下,可以用原子操作来替换重量级的锁同步,从而提高程序性能。原子操作可以保障多个线程或进程在更新某块共享内存区时,可以避免同步原语。

对于原子操作的实现来说,需要分开考虑单处理器单核系统,和多处理器系统,多核系统

对于单处理器单核系统来说,只要保证操作指令序列不被打断即可实现原子操作。对于简单的原子操作,cpu实现上会提供单条指令,比如INC和XCHG。对于复杂的原子操作,需要包含多条指令。执行过程中,出现上下文切换行为,比如任务切换,中断处理等。这里的行为会影响原子操作的原子性。因此需要自旋锁spinlock[1]来保证操作指令序列不会在执行的中途受干扰。

但是如果对于多处理器或者多核的系统,原子操作的实现除了需要spinlock来保证外,还需要保证不会受到同处理器上其他核,或者其他处理器的影响。当其他核上执行的指令访问的内存空间,与当前原子操作需要访问的内存空间存在冲突时,就会破坏原子操作的正确性。

在x86架构中,提供了指令前缀LOCK。LOCK保证了指令不会受其他处理器或cpu核的影响。在PentiumPro之前,LOCK的实现,是通过锁住bus(总线),从而阻止其他cpu核的内存访问。可想而知,这种实现是非常低效的。从PentiumPro开始,LOCK只会阻塞其他cpu核对相关内存的缓存块的访问。

现在,大多数的x86处理器都支持了CAS[2]的硬件实现,保证了多处理器多核系统下的原子操作的正确性。CAS的实现同样无需锁住总线,只会阻塞其他cpu核对相关内存的缓存块的访问。同样的,在MIPS和ARM架构下,还支持了LL/SC的实现[3]。LL/SC不会出现CAS中的ABA问题。

在继续深入以前,需要了解MESI缓存协议[4]。当然,还存在其他的MESI变种,不过这里只会简单解释下MESI。每个cache line存在四种状态,Modified代表该cache line为该cpu核独有,且尚未写回(write back)到内存(对缓存一致性不了解的看这里[5])。Exclusive代表该cache line为该cpu核独有,且与内存一致。Shared代表该cache line为多核共享,且与内存一致。Invalid代表缓存失效。系统中多个核之间通过快速通道直接通信,比如intel家的QPI,amd家的Hypertransport。cpu核之间通信的消息包括读消息,以及读消息的响应消息。使无效消息,以及使无效消息的响应消息。当运行在某个cpu核的线程准备读取某个cache line的内容时,如果状态处于M,E,S,直接读取即可。如果状态处于I,则需要向其他cpu核广播读消息,在接受到其他cpu核的读响应后,更新cache line,并将状态设置为S。而当线程准备写入某个cache line时,如果处于M状态,直接写入。如果处于E状态,写入并将cache line状态改为M。如果处于S,则需要向其他cpu核广播使无效消息,并进入E状态,写入修改,后进入M状态。如果处于I,则需要向其他cpu核广播读消息核使无效消息,在收集到读响应后,更新cache line。在收集到使无效响应后,进入E状态,写入修改,后进入M状态。

从上面的说明可知,LOCK的实现,只需要保持cache line的M状态即可,此时就可以阻止其他cpu核对该块内存的修改,而不用去锁住整个总线。

同理,CAS和LL/SC的实现,您应该可以猜出来了吧?

无锁CAS

并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加
锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次
数不得小于 3 次

透过linux内核看无锁编程

CAS 原语负责将某处内存地址的值(1 个字节)与一个期望值进行比较,如果相等,则将该内存地址处的值替换为新值,CAS 操作伪码描述如下 :


                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值