Golang sync.Mutex分析

sync.Mutex是一个不可重入的排他锁。当一个 goroutine 获得了这个锁的拥有权后, 其它请求锁的 goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放。

sync.Mutex数据结构

type Mutex struct {
	//状态
	state int32
	//控制锁状态的信号量
	sema  uint32
}

const (
    //锁的状态
	mutexLocked = 1 << iota // mutex is locked
    //正常模式唤醒
	mutexWoken
    //进入饥饿模式
	mutexStarving
	mutexWaiterShift      = iota
)

sync.Mutex锁的模式

 正常模式:所有等待锁的goroutine按照顺序等待

 饥饿模式:新来的goroutine不会去尝试获取锁,也不会进行自旋操作,会被放到等待队列的尾部

sync.Mutex断点调试

      首先先起两个goroutine在x.Lock()处下断点然后debug运行

 Lock

    这里可以看到我们刚刚2个goroutine一个获取到锁return一个进入lockSlow函数                        

                 

接下来让我们看一下lockSlow函数

               一步步点击红箭头指向的按钮,会进入到for循环接下来让我们一点点看for循环里面的逻辑

     

     

//是否满足自旋条件函数
func sync_runtime_canSpin(i int) bool {
    // i >= active_spin 当前go协程自旋状态小于4次  active_spin值为4
    //ncpu <= 1  当前电器cpu核数大于1
    //GOMAXPROCS>1,并且至少有一个正在运行P,而本地runq为空
	if i >= active_spin || ncpu <= 1 || gomaxprocs int32(sched.npidle+sched.nmspinning)+1 {
		return false
	}
	if p := getg().m.p.ptr(); !runqempty(p) {
		return false
	}
	return true
}

func (m *Mutex) lockSlow(){
	//goroutine等待时间
	var waitStartTime int64
	//是否是饥饿模式
	starving := false
	//goroutine是否被唤醒
	awoke := false
	//自旋次数
	iter := 0
	old := m.state
	for {
         //这里运用位操作
         //第一个条件:锁处于被锁状态并且不处于饥饿状态
         //第二个条件:是否满足自选条件  runtime_canSpin()函数
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {

			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			runtime_doSpin()
			iter++
			old = m.state
			continue
		}
         
        此案例m.status的状态只有1 2 3    
        status 1 锁还没有被释放,锁处于正常状态
        status 2 未解锁 但处于饥饿模式
        status 3  锁被释放处于饥饿模式
        status 4 锁已经被释放, 锁处于饥饿状态
 
		// new 用来设置新的状态
		// old 是锁当前的状态
		new := old
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
		// 将等待队列的等待者的数量加1
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}

		//切换的饥饿模式,如果已经解锁则不切换
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
		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
		}

		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			//锁已经被释放
			if old&(mutexLocked|mutexStarving) == 0 {
				break 
			}
		
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				//获取goroutine等待时间
				waitStartTime = runtime_nanotime()
			}
			//queueLifo = false 表示新来的goroutine放到队列后面等待
			//queueLifo = true  唤醒的goroutine放到队列第一位
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			//进入饥饿模式
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			if old&mutexStarving != 0 {
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
                  //锁置为正常模式
				if !starving || old>>mutexWaiterShift == 1 {
                    //退出饥饿模式
					delta -= mutexStarving
				}
				atomic.AddInt32(&m.state, delta)
				break
			}
             //唤醒go协程
			awoke = true
            //自旋次数置为0
			iter = 0
		} else {
			old = m.state
		}
	}

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



   接下来我们一直调试断点会发现如下状态

   我们会发现选择goroutine下拉框比上面少了goroutine6这是因为goroutine6运行完成释放了锁,这是正好对应status 3  锁被释放处于饥饿模式

   

   接下来在调试就会获得锁break跳出循环

     

 Unlock

   Unlock相对Lock要简单

func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	//把状态置未0
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {
        sync.Mutex.unlockSlow 方法开始慢速解锁
		m.unlockSlow(new)
	}
}

func (m *Mutex) unlockSlow(new int32) {
	if (new+mutexLocked)&mutexLocked == 0 {
		throw("sync: unlock of unlocked mutex")
	}
	//不处于饥饿模式
	if new&mutexStarving == 0 {
		old := new
		for {
            //如果mutexLocked、mutexStarving、mutexWoken 都不等于0直接return
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			//如果存在等待的,调用runtime_Semrelease移交控制权
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
		//处于饥饿模式直接,调用runtime_Semrelease移交控制权
		runtime_Semrelease(&m.sema, true, 1)
	}
}

 总结

  status有四种状态    
        status 1 锁还没有被释放,锁处于正常状态
        status 2 未解锁 但处于饥饿模式
        status 3  锁被释放处于饥饿模式
        status 4 锁已经被释放, 锁处于饥饿状态
自旋条件                                                                                         

      (1) i >= active_spin 当前go协程自旋状态小于4次  active_spin值为4
      (2) ncpu <= 1  当前电器cpu核数大于1
      (3)GOMAXPROCS>1,并且至少有一个正在运行P,而本地runq空                            

    (4)不处于饥饿模式
Lock                                                                                              

      1.正常模式                                                                                    

             判断是否满足自旋条件,处理完自旋条件,保存锁的状态到new,并根据mutexStarving.mutexLocked这几个不同的条件更新new的值,然后通过CAS更新锁的状态置,如果这时候锁的状态等于0并且不处于饥饿状态,则获取锁成功返回,通过runtime_nanotime获取等待时间调用runtime_SemacquireMutex保障不会被同事获取                                                          

      2.饥饿模式                                                                                     

饥饿模式下,没有自旋逻辑,新进来的goroutine会被放在队列最后面               
Unlock
        1.把锁的状态置为0,如果正常则释放锁成功
        2.如果释放锁失败则会调用unlockSlow慢解锁:                                                     

                (1) 正常模式下如果mutexLocked、mutexStarving、mutexWoken 都不等于0直接return,如果存在等待的,调用runtime_Semrelease移交控制权                                             

              (2)饥饿模式下直接调用runtime_Semrelease移交控制权                                                                                                               

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值