首页 > 编程知识 正文

amazongo原理,go语言底层是c语言吗

时间:2023-05-06 19:01:01 阅读:110741 作者:1964

go基础系列-Mutex原理分析文章目录go基础系列-Mutex原理分析前言Mutex数据结构Mutex方法和解锁过程什么是简单解锁和清醒协作自旋过程自旋? 自旋条件自旋优势自旋问题Mutex模式normal模式starvation模式woken状态为什么要重复解锁panic编程Tips rwmutex前言读写锁数据结构型定义接口定义Lock ()实现逻辑panic

首先,互斥锁是并发程序中对共享资源进行访问控制的主要手段,Go语言提供了一种非常简单、使用方便的Mutex Mutex,首先是3358www.Sina.com/对外暴露的两种方法3358www.Sina.com/

Mutex.state为结构体类型

例如Lock()和Unlock()等Mutex.sema是3358www.Sina.com/

协和式重复解锁引起panic解锁的协和式释放信号量并唤醒信号量等待,协和式Mutex.state是32位整数变量

内部安装时为了记录互斥锁的状态Mutex的http://www.Sina.com/下图表示mutex的存储器布局:

Locked:是此Mutex 是否被锁定

0 :未锁定1 :锁定的Woken:位于信号量

0 )没有协和式唤醒1 )协和式唤醒,处于锁定状态的Starving显示此Mutex 阻塞等待该信号量

0 )无饥饿1 )饥饿状态表明协和式飞行器阻塞Waiter:超过1ms,该变量分成四份

在康科德解锁时,根据该值判断信号量康科德之间是否需要解锁四种状态

可以在Locked域中设置1意味着,即使成功夺取了锁,如果抢不到的话,Mutex.sema信号也会暂时存储在http://www.Sina.com/http://www.Sina.com /

Mutex方法Mutext对外提供了两种方法,实际上只有这两种方法。 锁定(:锁定方法Unlock ) :解锁方法解锁和解锁流程是否已被锁定个案例http://www.Sina.com/http://www.Sina.com.com 根据,两种类型的处理和解锁过程的简单解锁过程如下图所示,假设当前只有一个已锁定的ACL,并且没有其他ACL的干扰。

锁定过程确定锁定标志位是否为0。 如果为0,则锁定位置1表示锁定成功。 假设锁定成功后只有锁定位置1,其他状态位未发生变化的锁定被阻塞并锁定,则锁定为是否有协程已被唤醒。 在这种情况下,锁定过程应如下图所示。

如上图所示,再次锁定Acconditer被占用的锁时,Waiter计数器将增加1,Acconditer b将阻塞,直到锁定值变为0。如果在解锁的简单前提下解除锁定,则不会阻塞其他Acconditer 此时,解锁过程如下图所示。

因为其他的协同学没有等待解锁,所以解锁时只将Locked位置设为0,不释放信号量就解锁,启动协同学假设解锁时,为是否处于饥饿状态,此时的解锁过程如下图所示

3358www.Sina.com/解锁过程分为两个步骤,一个是Locked位置0,两个是阻塞等待锁的协程个数,因此释放一个http://www.Sina.com,一个协和式b获得锁定旋转过程如果当前Locked比特为1,则当前尝试锁定实际上是抢给Locked赋值的权利该锁的协和式不会立即转到块,而是转到http://www.Sina.com

trong>这个过程即为自旋过程 自旋时间很短,但如果在自旋过程中发现锁已被释放 那么协程可以立即获取锁 此时即便有协程被唤醒也无法获取锁,只能再次阻塞自旋的好处是 当加锁失败时不必立即转入阻塞有一定机会获取到锁 这样可以避免协程的切换 什么是自旋? 自旋对应于CPU的”PAUSE”指令 CPU对该指令什么都不做,相当于CPU空转对程序而言相当于sleep了一小段时间,时间非常短当前实现是30个时钟周期 自旋过程中会持续探测Locked是否变为0 连续两次探测间隔就是执行这些PAUSE指令它不同于sleep不需要将协程转为睡眠状态 自旋条件 加锁时程序会自动判断是否可以自旋 无限制的自旋将会给CPU带来巨大压力所以判断是否可以自旋就很重要了 自旋必须满足以下所有条件: 自旋次数要足够小,通常为4 即自旋最多4次 CPU核数要大于1 否则自旋没有意义因为此时不可能有其他协程释放锁 协程调度机制中的Process数量要大于1 比如使用GOMAXPROCS()将处理器设置为1就不能启用自旋 协程调度机制中的可运行队列必须为空 否则会延迟协程调度 可见,自旋的条件是很苛刻的 总而言之就是不忙的时候才会启用自旋 自旋的优势 自旋的优势是 更充分的利用CPU,尽量避免协程切换 因为当前申请加锁的协程拥有CPU如果经过短时间的自旋可以获得锁当前协程可以继续运行,不必进入阻塞状态 自旋的问题 如果自旋过程中获得锁 那么之前被阻塞的协程将无法获得锁如果加锁的协程特别多,每次都通过自旋获得锁 那么之前被阻塞的进程将很难获得锁,从而进入饥饿状态 为了避免协程长时间无法获取锁 自1.8版本以来增加了一个状态,即Mutex的Starving状态这个状态下不会自旋 一旦有协程释放锁那么一定会唤醒一个协程并成功加锁 Mutex模式 前面分析加锁和解锁过程中只关注了Waiter和Locked位的变化现在我们看一下Starving位的作用每个Mutex都有两个模式 NormalStarving normal模式 默认情况下,Mutex的模式为normal,该模式下 协程如果加锁不成功 不会立即转入阻塞排队而是判断是否满足自旋的条件 如果满足则会启动自旋过程尝试抢锁 starvation模式 自旋过程中能抢到锁 一定意味着同一时刻有协程释放了锁 释放锁时如果发现有阻塞等待的协程 还会释放一个信号量来唤醒一个等待协程 被唤醒的协程得到CPU后开始运行 此时发现锁已被抢占了,自己只好再次阻塞 不过阻塞前会判断自上次阻塞到本次阻塞经过了多长时间如果超过1ms的话将Mutex标记为”饥饿”模式然后再阻塞 处于饥饿模式下 不会启动自旋过程也即一旦有协程释放了锁,那么一定会唤醒协程被唤醒的协程将会成功获取锁同时也会把等待计数减1 woken状态 Woken状态用于加锁和解锁过程的通信 举个例子:同一时刻,两个协程一个在加锁,一个在解锁 在加锁的协程可能在自旋过程中 此时把Woken标记为1用于通知解锁协程不必释放信号量了好比在说:你只管解锁好了,不必释放信号量,我马上就拿到锁了 为什么重复解锁要panic 可能你会想 为什么Go不能实现得更健壮些,多次执行Unlock()也不要panic? 仔细想想Unlock的逻辑就可以理解,这实际上很难做到 Unlock过程分为将Locked置为0然后判断Waiter值 如果值>0,则释放信号量 如果多次Unlock() 那么可能每次都释放一个信号量 这样会唤醒多个协程 多个协程唤醒后会继续在Lock()的逻辑里抢锁 势必会增加Lock()实现的复杂度也会引起不必要的协程切换。 编程Tips 使用defer避免死锁 加锁后立即使用defer对其解锁可以有效的避免死锁 加锁和解锁应该成对出现 加锁和解锁最好出现在同一个层次的代码块中 比如同一个函数 重复解锁会引起panic 应避免这种操作的可能性 rwmutex 前言

所谓读写锁RWMutex

完整的表述应该是读写互斥锁

可以说是Mutex的一个改进版

在某些场景下可以发挥更加灵活的控制能力

比如:读取数据频率远远大于写数据频率的场景例如,程序中写操作少而读操作多

简单的说,如果执行过程是1次写然后N次读的话

使用Mutex,这个过程将是串行的

因为即便N次读操作互相之间并不影响但也都需要持有Mutex后才可以操作

如果使用读写锁

多个读操作可以同时持有锁

并发能力将大大提升

实现读写锁需要解决如下几个问题:

写锁需要阻塞写锁: 一个协程拥有写锁时,其他协程写锁定需要阻塞 写锁需要阻塞读锁: 一个协程拥有写锁时,其他协程读锁定需要阻塞 读锁需要阻塞写锁: 一个协程拥有读锁时,其他协程写锁定需要阻塞 读锁不能阻塞读锁: 一个协程拥有读锁时,其他协程也可以拥有读锁 读写锁数据结构 类型定义

由以上数据结构可见 读写锁内部仍有一个互斥锁 用于将两个写操作隔离开来 其他的几个都用于隔离读操作和写 操作 接口定义 RLock():读锁定RUnlock():解除读锁定Lock(): 写锁定 与Mutex完全一致 Unlock():解除写锁定 与Mutex完全一致 Lock()实现逻辑 写锁定操作需要做两件事: 获取互斥锁阻塞等待所有读操作结束(如果有的话) 所以 func (rw RWMutex) Lock() 接口实现流程如下图所示:

Unlock()实现逻辑 解除写锁定要做两件事: 唤醒因读锁定而被阻塞的协程(如果有的话)解除互斥锁

所以 func (rw RWMutex) Unlock() 接口实现流程如下图所示:

RLock()实现逻辑 读锁定需要做两件事: 增加读操作计数 即readerCount++ 阻塞等待写操作结束(如果有的话) 所以 func (rw RWMutex) RLock() 接口实现流程如下图所示:

RUnlock()实现逻辑 解除读锁定需要做两件事: 减少读操作计数 即readerCount— 唤醒等待写操作的协程(如果有的话) 所以 func (rw RWMutex) RUnlock() 接口实现流程如下图所示:
场景分析

上面我们简单看了下4个接口实现原理,接下来我们看一下是如何解决前面提到的几个问题的。

写操作是如何阻止写操作的 读写锁包含一个互斥锁(Mutex) 写锁定必须要先获取该互斥锁 如果互斥锁已被协程A获取(或者协程A在阻塞等待读结束) 那么协程B只能阻塞等待该互斥锁。 所以写操作依赖互斥锁阻止其他的写操作 写操作是如何阻止读操作的 这个是读写锁实现中最精华的技巧我们知道RWMutex.readerCount是个整型值 用于表示读者数量不考虑写操作的情况下,每次读锁定将该值+1, 每次解除读锁定将该值-1 所以readerCount取值为[0, N],N为读者个数实际上最冷艳的便当支持2^30个并发读者 当写锁定进行时 会先将readerCount减去2^30从而readerCount变成了负值此时再有读锁定到来时检测到 readerCount为负值便知道有写操作在进行只好阻塞等待 而真实的读操作个数并不会丢失 只需要将 readerCount加上2^30即可获得 所以,写操作将readerCount变成负值来阻止读操作的 读操作是如何阻止写操作的 读锁定会先将RWMutext.readerCount加1 此时写操作到来时发现读者数量不为0会阻塞等待所有读操作结束 所以,读操作通过readerCount来将来阻止写操作的 为什么写锁定不会被饿死 我们知道,写操作要等待读操作结束后才可以获得锁 写操作等待期间可能还有新的读操作持续到来如果写操作等待所有读操作结束,很可能被饿死 然而,通过RWMutex.readerWait可完美解决这个问题 写操作到来时 会把RWMutex.readerCount值拷贝到RWMutex.readerWait中 用于标记排在写操作前面的读者个数 前面的读操作结束后 除了会递减RWMutex.readerCount还会递减RWMutex.readerWait值当 RWMutex.readerWait值变为0时唤醒写操作 所以,写操作就相当于把一段连续的读操作划分成两部分 前面的读操作结束后唤醒写操作写操作结束后唤醒后 面的读操作 如下图所示:

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。