golang单机锁实现

打印 上一主题 下一主题

主题 914|帖子 914|积分 2744

1、锁的概念引入

起首,为什么必要锁?
在并发编程中,多个线程或历程可能同时访问和修改同一个共享资源(比方变量、数据结构、文件)等,若不引入合适的同步机制,会引发以下问题:

  • 数据竞争:多个线程同时修改一个资源,最终的结果跟线程的实行次序有关,结果是不可猜测的。
  • 数据不一致:一个线程在修改资源,而另一个线程读取了未修改完的数据,从而导致读取了错误的数据。
  • 资源竞争:多线程竞争同一个资源,浪费系统的性能。
因此,我们必要一把锁,来保证同一时间只有一个人能写数据,确保共享资源在并发访问下的正确性和一致性。
在这里,引入两种常见的并发控制处理机制,即乐观锁悲观锁

  • 乐观锁:假定在并发操作中,资源的抢占并不是很猛烈,数据被修改的可能性不是很大,那这时候就不必要对共享资源区进行加锁再操作,而是先修改了数据,最终来判定数据有没有被修改,没有被修改则提交修改指,否则重试。
  • 悲观锁:与乐观锁相反,它假设场景的资源竞争猛烈,对共享资源区的访问必须要求持有锁。
针对差别的场景必要采取因地制宜的策略,比较乐观锁与悲观所,它们的优缺点显而易见:
策略优点缺点乐观锁不必要实际上锁,性能高若冲突时,必要重新进行操作,多次重试可能会导致性能降落明显悲观锁访问数据一定必要持有锁,保证并发场景下的数据正确性加锁期间,其他等候锁的线程必要被阻塞,性能低2、Sync.Mutex

Go对单机锁的实现,考虑了实际情况中协程对资源竞争程度的变革,制定了一套锁升级的过程。具体方案如下:

  • 起首采取乐观的态度,Goroutine会保持自旋态,通过CAS操作尝试获取锁。
  • 当多次获取失败,将会由乐观态度转入悲观态度,判定当前并发资源竞争程度剧烈,进入阻塞态等候被唤醒。
乐观转向悲观的判定规则如下,满足其中之一即发生转变:

  • Goroutine自旋尝试次数超过4次
  • 当前P的实行队列中存在等候被实行的G(避免自旋影响GMP调度性能)
  • CPU是单核的(其他Goroutine实行不了,自旋无意义)
除此之外,为了防止被阻塞的协程等候过长时间也没有获取到锁,导致用户的整体体验降落,引入了饥饿的概念:

  • 饥饿态:若Goroutine被阻塞等候的时间>1ms,则这个协程被视为处于饥饿状态
  • 饥饿模式:表现当前锁是否处于特定的模式,在该模式下,锁的交接是公平的,按次序交给等候最久的协程。
饥饿模式与正常模式的转变规则如下:

  • 普通模式->饥饿模式:存在阻塞的协程,阻塞时间超过1ms
  • 饥饿模式->普通模式:阻塞队列清空,亦大概获得锁的协程的等候时间小于1ms,则恢复
接下来步入源码,观看具体的实现。
2.1、数据结构

位于包sync/mutex.go中,对锁的定义如下:
  1. type Mutex struct {
  2.         state int32
  3.         sema  uint32
  4. }
复制代码

  • state:标识如今锁的状态信息,包括了是否处于饥饿模式、是否存在唤醒的阻塞协程、是否上锁、以及处于等候锁的协程个数有多少。
  • seme:用于阻塞和唤醒协程的信号量。
将state看作一个二进制字符串,它存储信息的规则如下:

  • 第一位标识是否处于上锁,0表现否,1表现上锁(mutexLocked)
  • 第二位标识是否存在唤醒的阻塞协程(mutexWoken)
  • 第三位标识是否处于饥饿模式(mutexStarving)
  • 从第四位开始,记录了处于阻塞态的协程个数
  1. const (
  2.         mutexLocked = 1 << iota // mutex is locked
  3.         mutexWoken
  4.         mutexStarving
  5.         mutexWaiterShift = iota
  6.         starvationThresholdNs = 1e6 //饥饿阈值
  7. )
复制代码
(2)进入尝试获取锁的循环中,两个if表现:
<ul>若锁处于上锁状态,并且不处于饥饿状态中,并且当前的协程允许继续自旋下去(非单核CPU、自旋次数>mutexWaiterShift == 0 {                                        throw("sync: inconsistent mutex state")                                }                //将要更新的信号量                                delta := int32(mutexLocked - 1mutexWaiterShift == 1 {                                        delta -= mutexStarving                                }                                atomic.AddInt32(&m.state, delta)                                break                        }                        awoke = true                        iter = 0        //....                } else {                        //...                }[/code]从阻塞中唤醒,起首计算一些协程的阻塞时间,以及当前的最新锁状态。
锁处于饥饿模式:那么当前协程将直接获取锁,当前协程是因为饥饿模式被唤醒的,不存在其他协程抢占锁。于是更新信号量,将记录阻塞协程数-1,将锁的上锁态置1。若当前从饥饿模式唤醒的协程,等候时间已经不到1ms了大概是最后一个等候的协程,那么将将锁从饥饿模式转化为正常模式。至此,获取成功,退出函数。
否则,只是普通的随机唤醒,于是开始尝试进行抢占,回到步骤1。
2.4、释放锁Unlock()
  1. func (m *Mutex) Lock() {
  2.         if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
  3.                 return
  4.         }
  5.         m.lockSlow()
  6. }
复制代码
通过原子操作,直接将锁的mutexLocked标识置为0。若置0后,锁的状态不为0,那就说明存在必要获取锁的协程,步入unlockSlow。
2.5、unlockSlow()

[code]func (m *Mutex) unlockSlow(new int32) {        if (new+mutexLocked)&mutexLocked == 0 {                fatal("sync: unlock of unlocked mutex")        }        if new&mutexStarving == 0 {                old := new                for {                        if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {                                return                        }                        new = (old - 1
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

用多少眼泪才能让你相信

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表