首页 > 其他分享 >golang的mutex互斥锁

golang的mutex互斥锁

时间:2023-01-04 14:02:36浏览次数:37  
标签:缓存 原子 golang 互斥 mutex 处理器 操作 内存

什么是计算机的锁?

以前经常遇到锁的时候,计算机的锁到底是一个普通变量,还是一个数据总线的一个开关。网上查,一上来都是一大推的云里雾里专业术语。看了也不懂,怪本人计算机知识浅薄。今天尝试用自己的理解加上资料中进行对锁基本认识。

从golang的mutex声明开始说起。Mutex是一个相互排斥的锁。从下面mutex源码知道是一个结构体,结构体包含两个变量,state和sema,其中state是整型32位,sema是无符号整型32位。从这里我们得到锁就是一个变量。

type Mutex struct {
state int32
sema uint32
}


一个state是4个字节的32位整型。如图:

golang的mutex互斥锁_内存地址

其中第一位用作锁状态标识符,置为1就表示已加锁,对应的掩码常量为mutexLocked。

第二位用于记录是否已有goroutine被唤醒了,置为1表示已唤醒,对应掩码常量为mutexWoken。

第三位标识Mutex的工作模式,0代表正常模式,1代表饥饿模式,对应掩码常量为mutexStarving。

而常量mutexWaiterShift等于3,表示除了最低三位以外,state的其他位用来记录有多少个等待着在排队。


Mutex怎么实现锁功能?

假设仅仅靠mutex结构体的两个变量就能实现计算机锁功能,那么就毫无意义去研究,跟平时开发过程,设置某一个标记变量var i=0,i为1时,就锁住没有任何区别。

Mutex肯定依赖计算机某一些约定或者某一种硬件技术来实现锁的功能。

32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。

先了解计算机一些术语(这些术语就是底层CPU一些功能介绍,还是必须了解一下)

术语

英文单词

术语描述

内存屏障

memory barriers

是一组处理器指令、用于实现对内存操作的顺序限制


缓存行

cache line

cpu高速缓存中可以分配的最小存储单位,处理器填写缓存行时会加载整个缓存行,现代CPU需要执行几百次cpu指令。

原子操作

atomic operations

不可中断的一个或一系列操作

缓存行填充

cache line fill

当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个高速缓存行到适当的缓存(L1,L2,L3的或者所有)

缓存命中

cache hit

如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取

写命中

write hit

当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作称为写操作。

写缺失

write misses the cache

一个有效缓存行写入到一个不存在的内存区域

比较并交换

compare and swap

CAS操作需要输入两个数值,一个是旧数值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有变化,如果没有发生变化,才交换成为新值,发生了变化则不交换

CPU流水线

cpu pipeline

CPU流水线的工作方式就像工业生产上的装配流水线,在CPU中由5-6个不同功能的电路单元组成一条指令处理流水线,然后将一条X86指令分成5-6步后再由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度。

内存顺序冲突

memory order violation

内存顺序冲突一般由假共享引起的,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线

总线加锁原理

golang的mutex互斥锁_内存地址_02

如果多个处理器一起对共享变量进行读改写操作(i++就是典型的读改写操作),这个读改写操作就不是原子的。如上图所示,各个处理器将i的值读入自己的处理器缓存中,各自对各自的缓存里的i值进行操作,然后分别写入系统内存从而导致了问题的产生。要想对共享变量的读改写操作也是原子性的,必须保证,CPU1读改写共享变量的时候,CPU2不能操作该共享变量内存地址的缓存行。

        处理器的总线锁就是这么来保证原子性的,所谓总线锁就是使用处理器提供的一个LOCK #信号,当一个处理器在总线上输出此信号时,其他处理器的请求将会被阻塞住,那么该处理器可以独占共享内存。

缺点:在同一时刻我们只需要保证对某一个内存地址的操作是原子性就可以了,但是总线锁把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。

缓存加锁原理

频繁使用的内存会缓存在处理器的L1,L2,L3高速缓存中,那么原子操作就可以直接在处理器内部缓存进行,并不需要声明总线锁。

缓存锁是:一个处理器的缓存写回到内存会导致其他处理器的缓存无效,在多核处理器中,例如在Pentium和P6  family处理中,如果通过嗅探一个处理器来检测到其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

        但是有两种情况是不能使用缓存锁:一是不能缓存到处理器的数据以及跨多个缓存行的数据;而是有些处理器不支持缓存锁定。

go的mutex原理:通过CAS原子操作来实现,是一个轻量级的锁。

我们先来看看mutex结构体定义了两个方法,其中一个是Lock方法,

从代码中看到atomic.CompareAndSwapInt32进行state修改,以达到锁的功能。

func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}

这里使用sync/atomic标准库包中提供的原子操作.原子操作是比其它同步技术更基础的操作。原子操作是无锁的,常常直接通过CPU指令直接实现。 事实上,其它同步技术的实现常常依赖于原子操作。原子操作可确保这些​​goroutine​​之间不存在数据竞争。

至于程序分析,网上有大把资料做分析。这里只做锁的基本理解。细节参考其他资料。


原子操作与互斥锁的区别

互斥锁是一种数据结构,使你可以执行一系列互斥操作。而原子操作是互斥的单个操作,这意味着没有其他线程可以打断它。那么就Go语言里atomic包里的原子操作和sync包提供的同步锁有什么不同呢?

首先atomic操作的优势是更轻量,比如CAS可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。这可以大大的减少同步对程序性能的损耗。

原子操作也有劣势。还是以CAS操作为例,使用CAS操作的做法趋于乐观,总是假设被操作值未曾被改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换,那么在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。而使用互斥锁的做法则趋于悲观,我们总假设会有并发的操作要修改被操作的值,并使用锁将相关操作放入临界区中加以保护。

所以总结下来原子操作与互斥锁的区别有:

  • 互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。
  • 原子操作是针对某个值的单个互斥操作
  • 可以把互斥锁理解为悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

atomic包提供了底层的原子性内存原语,这对于同步算法的实现很有用。这些函数一定要非常小心地使用,使用不当反而会增加系统资源的开销,对于应用层来说,最好使用通道或sync包中提供的功能来完成同步操作。





标签:缓存,原子,golang,互斥,mutex,处理器,操作,内存
From: https://blog.51cto.com/wyf1226/5988349

相关文章