golang的sync.mutex

黑魔术师 · · 2497 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

文章计划分为以下几部分:

1.什么是锁,锁的粒度,一般锁的实现,常见锁的类型

并发下的同步和互斥

原子操作、信号量、互斥量、读写信号量、自旋锁

2.golang锁的设计思想与演进

3.golang锁设计的底层基础简介

4.golang锁的代码实现与一些流程图

5.吐槽~

参考资料:

设计演进http://www.cnblogs.com/niniwzw/archive/2013/06/24/3153955.html

一、什么是并发锁

高并发下,如何保证并行的多线程同时访问一片共享内存时不出现问题,加锁就是最简单的一种解决方案,通过保证上锁的原子操作来保证对资源访问权限的控制,其余的内存屏障等途径留待后续整理。

锁在不同场景下可以有很多分类,比如,分为乐观锁(读写锁)和悲观锁(互斥锁)

分为自旋锁、阻塞锁、重入锁、偏向锁、轻量锁和重量锁等。

锁的实现一般会依赖于信号量,信号量则是一个非负的整数计数器。

信号量:多线程同步使用的;一个线程完成某个动作后通过信号告诉别的线程,别的线程才可以执行某些动作;非负整数
互斥量:多线程互斥使用的;一个线程占用某个资源,那么别的线程就无法访问,直到该线程离开,其他线程才可以访问该资源;0或1

二、普通并发锁的设计思想

因为sync.mutex是悲观锁,也就是互斥锁,常规的互斥锁,可以通过信号量来进行控制。

具体互斥锁的实现原理可以参考这篇文章:https://www.cnblogs.com/sylz/p/6030201.html

简单来说,就是加锁时,就把信号量减一,如果是零说明加锁成功。释放锁时把信号量加一,如果是一说明释放成功。

按照这个场景,信号量和互斥量理论都可以完成,但是在实际应用中大家都使用信号量,因为信号量是多值得,可以通过信号量加等待队列,减少唤醒的次数。

三、golang的sync.mutex代码(1.10.3)

参考:https://studygolang.com/articles/1472

sema论文:https://swtch.com/semaphore.pdf

3.1mutex的代码

// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
type Mutex struct {
   state int32
   sema  uint32
}

从代码可以看到Mutex的核心由两部分组成,state负责统计锁的抢占着,sema通过信号量来进行goroutine的调度等操作。

mutexLocked = 1(1):表示mutex处于锁状态。

mutexWoken = 2(10):表示mutex处于唤醒状态。

mutexStarving = 4 (100) :标识处于饥饿状态。(饥饿状态在此场景下应该怎么理解)

mutexWaiterShift = 3 :表示等待持有锁需要累计计数的左移位。

3.2Lock的代码

<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
func (m *Mutex) Lock() {
   //1.最简单的情况,无人锁定时的加锁。
   // Fast path: grab unlocked mutex.
   if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
      //可以忽略,这部分是特殊模式的代码
      if race.Enabled {
         race.Acquire(unsafe.Pointer(m))
      }
      return
   }

   //2.如果锁定失败,情况就比较多了
   var waitStartTime int64
   starving := false // 是否是优先调度?
   awoke := false // 是否是唤醒的
   iter := 0
   old := m.state
   for {
      // Don't spin in starvation mode, ownership is handed off to waiters
      // so we won't be able to acquire the mutex anyway.
      //2.1饥饿模式不允许自旋,控制权会被移交给等待方,非饥饿模式下允许自旋
      if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
         // Active spinning makes sense.
         // Try to set mutexWoken flag to inform Unlock
         // to not wake other blocked goroutines.
         //2.1.1 在非awoke状态下,且没有被唤醒状态下,且没有排队的情况下,把状态记为awoke,防止其他自旋等待
         if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
            atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
            awoke = true
         }
         //2.1.2 自旋等待,少量的同时自旋是可以容忍的。
         runtime_doSpin()
         iter++
         old = m.state
         continue
      }
      // 2.2 进入此处有两种情况,一种是未上锁状态,一种是自旋次数已经耗尽
      // for循环是为了一次cas修改不成功时,自动进行下次cas修改
      new := old
      // Don't try to acquire starving mutex, new arriving goroutines must queue.
      // 2.2.1如果没有优先状态的,才进行上锁
      if old&mutexStarving == 0 {
         new |= mutexLocked
      }
      // 2.2.2如果现在在上锁状态,或者在优先调度状态,就增加一个等待标识
      if old&(mutexLocked|mutexStarving) != 0 {
         new += 1 << mutexWaiterShift
      }
      // The current goroutine switches mutex to starvation mode.
      // But if the mutex is currently unlocked, don't do the switch.
      // Unlock expects that starving mutex has waiters, which will not
      // be true in this case.
      // 2.2.3如果这是次优先调度,且还是上锁状态,就加抢占标签,把其他上锁操作
      if starving && old&mutexLocked != 0 {
         new |= mutexStarving
      }
      // 2.2.4 如果是唤醒状态的,且有唤醒标识,这次去掉
      if awoke {
         // The goroutine has been woken from sleep,
         // so we need to reset the flag in either case.
         if new&mutexWoken == 0 {
            throw("sync: inconsistent mutex state")
         }
         new &^= mutexWoken
      }
      // 2.2.5 根据判断,设置新的状态,包括上锁成功和未上锁成功,加入排队序列等
      if atomic.CompareAndSwapInt32(&m.state, old, new) {
         // 2.2.5.1 上锁成功,就可以结束上锁
         if old&(mutexLocked|mutexStarving) == 0 {
            break // locked the mutex with CAS
         }
         // If we were already waiting before, queue at the front of the queue.
         // 2.2.5.2 判断是否是被唤醒的未成功上锁的请求,如果是,加入队列头而不是队列尾部。
         queueLifo := waitStartTime != 0
         if waitStartTime == 0 {
            waitStartTime = runtime_nanotime()
         }
         // 2.2.5.3 加入信号量队列,包括sleep和awoke等
         runtime_SemacquireMutex(&m.sema, queueLifo)
         // 2.2.5.4 如果等待超过1e6纳秒,就进入抢占状态,优先调度
         starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
         old = m.state
         // 2.2.5.5 如果已经在抢占状态下的处理,
         if old&mutexStarving != 0 {
            // If this goroutine was woken and mutex is in starvation mode,
            // ownership was handed off to us but mutex is in somewhat
            // inconsistent state: mutexLocked is not set and we are still
            // accounted as waiter. Fix that.
            // 如果又是抢占,又上锁了?这种状态的理解需要仔细分析并发下的状态流转来实现
            if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
               throw("sync: inconsistent mutex state")
            }
            delta := int32(mutexLocked - 1<<mutexWaiterShift)
            if !starving || old>>mutexWaiterShift == 1 {
               // Exit starvation mode.
               // Critical to do it here and consider wait time.
               // Starvation mode is so inefficient, that two goroutines
               // can go lock-step infinitely once they switch mutex
               // to starvation mode.
               delta -= mutexStarving
            }
            atomic.AddInt32(&m.state, delta)
            break
         }
         awoke = true
         iter = 0
      } else {
         old = m.state
      }
   }

   if race.Enabled {
      race.Acquire(unsafe.Pointer(m))
   }
}

看完lock的代码,除去映射到runtime的自旋和信号量相关代码,我们已经可以完整的理解整个加锁的过程。对于自旋和信号量两部分的代码,估计需要之后结合golang的调度再做分析。

3.3Unlock代码

// Unlock unlocks m.
// It is a run-time error if m is not locked on entry to Unlock.
//
// A locked Mutex is not associated with a particular goroutine.
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
func (m *Mutex) Unlock() {
   // 特殊模式代码
   if race.Enabled {
      _ = m.state
      race.Release(unsafe.Pointer(m))
   }

   // Fast path: drop lock bit.
   new := atomic.AddInt32(&m.state, -mutexLocked)
   if (new+mutexLocked)&mutexLocked == 0 {
      throw("sync: unlock of unlocked mutex")
   }
   if new&mutexStarving == 0 {
      old := new
      for {
         // If there are no waiters or a goroutine has already
         // been woken or grabbed the lock, no need to wake anyone.
         // In starvation mode ownership is directly handed off from unlocking
         // goroutine to the next waiter. We are not part of this chain,
         // since we did not observe mutexStarving when we unlocked the mutex above.
         // So get off the way.
         if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
            return
         }
         // Grab the right to wake someone.
         new = (old - 1<<mutexWaiterShift) | mutexWoken
         if atomic.CompareAndSwapInt32(&m.state, old, new) {
            runtime_Semrelease(&m.sema, false)
            return
         }
         old = m.state
      }
   } else {
      // Starving mode: handoff mutex ownership to the next waiter.
      // Note: mutexLocked is not set, the waiter will set it after wakeup.
      // But mutex is still considered locked if mutexStarving is set,
      // so new coming goroutines won't acquire it.
      runtime_Semrelease(&m.sema, true)
   }
}

解锁的代码相比加锁要简单的多,看懂了加锁代码,解锁代码的逻辑应该就很容易理解。

四、反思

sync.mutex的代码如果直接看1.10.3是非常难懂的,因为代码已经越来越复杂,加入了starve等概念,增加了代码的复杂度。如果可以,最好从中抽象出不带starve的代码,精简程度大大增加(参考文章中的1.3),通过精简版本,我们可以更快的理解其主流程。看懂主流程之后再回归到现有版本,会让我们的理解变得更加容易。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:黑魔术师

查看原文:golang的sync.mutex

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2497 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传