Mutex

互斥锁

简单版本:锁计数locks,信号量sem
1、加锁:

if atomicCompareAndSwap(locks,0,1)  then
  // 加锁成功
else then
  插入sem等待队列尾部
end

2、解锁

if atomicCompareAndSwap(locks,1,0) > 0 then
  尝试唤醒sem等待队列头部
end

该策略存在优化点:
1、活跃的协程获得锁时可尝试短暂自旋等待,不切换上下文
2、第1点带来的问题,活跃的协程频繁获得锁时会导致队列中任务等待时间过长,为此增加了饥饿模式

数据结构

// 源码路径:src/sync/mutex.go
type Mutex struct {
    state int32 // 状态state。低3位分别:Lock加锁、Starve饥饿、Wake唤醒;高位:等待的任务数。如state.png
    sema  uint32 // 信号量
}

state.png

加锁

// 源码路径:src/sync/mutex.go
// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
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()
}

func (m *Mutex) lockSlow() {
    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.
        // 饥饿模式下不自旋
        // 普通模式下尝试自旋。多核可自旋不超过4次
        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.
            // 标记唤醒状态
            if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
                atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
                awoke = true
            }
            runtime_doSpin()
            iter++
            old = m.state
            continue
        }
        new := old
        // Don't try to acquire starving mutex, new arriving goroutines must queue.
        // 饥饿状态下仅唤醒等待队列中任务加锁
        if old&mutexStarving == 0 {
            new |= mutexLocked
        }
        // 已加锁或饥饿状态,直接等待队列计数+1
        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.
        // 当前的任务被唤醒进入饥饿状态
        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 // locked the mutex with CAS
            }
            // If we were already waiting before, queue at the front of the queue.
            queueLifo := waitStartTime != 0 // 先进后出
            if waitStartTime == 0 {
                waitStartTime = runtime_nanotime()
            }
            // 加入等信号量等待队列
            runtime_SemacquireMutex(&m.sema, queueLifo, 1)
            starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
            old = m.state
            // 被信号量唤醒后且处于饥饿状态
            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<唤醒,进行调度优化,比较耗性能。
                // 当前被唤醒的任务非饥饿状态或等待队列仅1个任务,可退出饥饿模式
                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))
    }
}

解锁

/ 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 != 0 {
        // Outlined slow path to allow inlining the fast path.
        // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
        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 {
            // 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.
            // 尝试唤醒任务,等待任务计数-1
            new = (old - 1<

读写锁

数据结构

读写锁是针对读多写少的场景。
读写锁需要记录多个数据:
1、正在读任务的数量 readCount。readCount<0:存在写任务
2、读任务信号量readSem。正在挂起的读队列
3、写任务信号量writeSem。正在挂起的写队列
4、读等待任务的数量readWait
5、互斥锁w

RLock 读加锁

// >0:加锁成功
// <0:存在写任务
if atomicAdd(readCount,1) < 0 then
   readSem入队睡眠
end

RUnlock 读解锁

// >0:解锁成功
// <0:存在写任务
if atomicAdd(readCount,-1) < 0 then
   // 0:当前最后一个读任务,可以唤醒写任务
  if atomicAdd(readWait,-1) == 0 then
    writeSem出队唤醒
  end
end

Lock 写加锁

w.Lock()
r := atomicAdd(readCount,-writeShift)+writeShift
if r!=0 && atomicAdd(readWait,r) != 0 then
  writeSem入队
end

Lock 写解锁

r := atomicAdd(readCount,writeShift)
for i=1,r {
  readSem出队
}
w.Unlock()

你可能感兴趣的:(Mutex)