Golang lock implementation principle

what is a lock

  • The essence of a lock is a resource, which is a resource specially used for synchronization maintained by the operating system
  • For example, a mutex, to put it bluntly, is a mutually exclusive resource. Only one process (thread) can occupy it. When a process (thread) acquires a lock through competition, other processes (or threads) will not get the lock. This is determined by the kernel code
  • If we want a certain resource to be shared among multiple processes (threads/coroutines), but at most one process occupies it at a certain moment, isn't this the concept of a mutex, that is to say, we want our own resources to also be shared? become a lock
  • The easiest way is to bind your own resources to locks defined by the operating system. That is to say, before the process can obtain my resources, it must obtain the lock of the operating system. Furthermore, if you have to lock, you will get resources, but if you lose the lock, you will lose resources. In this case, our resource becomes a lock

Why use locks

Guaranteeing data consistency and security in concurrent programming

Locks in Golang

image

The synchronization mechanisms provided by Golang include Mutex, WaitGroup under the sync module, and chan provided by the language itself. These synchronization methods are based on the underlying synchronization mechanisms (cas, atomic, spinlock, sem) implemented in the runtime

1. cas、atomic

cas (Compare And Swap) and atomic operations are the basis of other synchronization mechanisms

  • Atomic operation: those operations that cannot be interrupted are called atomic operations. When one CPU is accessing the content addr, other CPUs cannot access it.
  • CAS: comparison and exchange are actually atomic operations, but they are non-blocking, so when the value being operated is changed frequently, the CAS operation is not so easy to succeed, and it is necessary to use the for loop to make multiple attempts

2. Spinlock (spinlock)

A spin lock means that when a thread acquires a lock, if the lock has been acquired by other threads, then the thread will wait in a loop, and then constantly judge whether it can be successfully acquired, and will not exit the loop until the lock is acquired. The thread that acquires the lock is always active.
The spin lock in Golang is used to implement other types of locks. It is similar to a mutex. The difference is that it does not block the process by sleeping, but is always active until the lock is acquired. state (spin)

3. Signal amount

A way to implement sleep and wake up coroutines

信号量有两个操作P和V
P(S):分配一个资源
1. 资源数减1:S=S-1
2. 进行以下判断
    如果S<0,进入阻塞队列等待被释放
    如果S>=0,直接返回

V(S):释放一个资源
1. 资源数加1:S=S+1
2. 进行如下判断
    如果S>0,直接返回
    如果S<=0,表示还有进程在请求资源,释放阻塞队列中的第一个等待进程
    
golang中信号量操作:runtime/sema.go
P操作:runtime_Semacquire
V操作:runtime_Semrelease

The use of mutex

package main

import (
    "fmt"
    "sync"
)

var num int
var mtx sync.Mutex
var wg sync.WaitGroup

func add() {
    
    
    mtx.Lock()  //mutex实例无需实例化,声明即可使用

    defer mtx.Unlock()
    defer wg.Done()

    num += 1
}

func main() {
    
    
    for i := 0; i < 100; i++ {
    
    
        wg.Add(1)
        go add()
    }

    wg.Wait()

    fmt.Println("num:", num)
}

The necessity of mutex

When the lock is highly competitive, it will continue to suspend the recovery thread to give up cpu resources. Atomic variables will always occupy the cpu when highly competitive; atomic operations are thread-level and do not support coroutines.

mutex evolution

1. Mutex

type Mutex struct {
    
    
    state int32
    sema  uint32
}

const (
    mutexLocked = 1 << iota
    mutexWoken
    mutexWaiterShift = iota  //根据 mutex.state >> mutexWaiterShift 得到当前等待的 goroutine 数目
)

state represents the state of the current lock and is a shared variable

state:  |32|31|....|3|2|1|
         \__________/ | |
               |      | |
               |      | 当前mutex是否加锁
               |      |
               |      当前mutex是否被唤醒
               |
               等待队列的goroutine协程数

There are two situations when the Lock method applies for locking the mutex

  1. No conflict Set the current state to the locked state through the CAS operation
  2. There is a conflict. Let the current goroutine go to sleep by calling the semacquire function, and wake up when other coroutines release the lock
//如果已经加锁,那么当前协程进入休眠阻塞,等待唤醒
func (m *Mutex) Lock() {
    
    

    // 快速加锁:CAS更新state为locked
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    
    
        return
    }

    awoke := false //当前goroutine是否被唤醒
    for {
    
    
        old := m.state // 保存当前state的状态
        new := old | mutexLocked // 新值locked位设置为1
        // 如果当前处于加锁状态,新到来的goroutine进入等待队列
        if old&mutexLocked != 0 {
    
    
            new = old + 1<<mutexWaiterShift
        }
        if awoke {
    
    
            //如果被唤醒,新值需要重置woken位为 0
            new &^= mutexWoken
        }
        
        // 两种情况会走到这里:1.休眠中被唤醒 2.加锁失败进入等待队列
        // CAS 更新,如果更新失败,说明有别的协程抢先一步,那么重新发起竞争。
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
    
    
            // 如果更新成功,有两种情况
            // 1.如果为 1,说明当前 CAS 是为了更新 waiter 计数
            // 2.如果为 0,说明是抢锁成功,那么直接 break 退出。
            if old&mutexLocked == 0 {
    
     
                break
            }
            runtime_Semacquire(&m.sema) // 此时如果 sema <= 0 那么阻塞在这里等待唤醒,也就是 park 住。走到这里都是要休眠了。
            awoke = true  // 有人释放了锁,然后当前 goroutine 被 runtime 唤醒了,设置 awoke true
        }
    }

    if raceenabled {
    
    
        raceAcquire(unsafe.Pointer(m))
    }
}

UnLock is unlocked in two steps

  1. Unlock , set the current state to unlock state through CAS operation
  2. Wake up the dormant coroutine , the CAS operation reduces the number of waiters in the current state by 1, and then wakes up the dormant goroutine
//锁没有和某个特定的协程关联,可以由一个协程lock,另一个协程unlock
func (m *Mutex) Unlock() {
    
    
    if raceenabled {
    
    
        _ = m.state
        raceRelease(unsafe.Pointer(m))
    }

    // CAS更新state的状态为locked 注意:解锁的瞬间可能会有新的协程到来并抢到锁
    new := atomic.AddInt32(&m.state, -mutexLocked)
    // 释放了一个没上锁的锁会panic:原先的lock位为0
    if (new+mutexLocked)&mutexLocked == 0 {
    
     
        panic("sync: unlock of unlocked mutex")
    }
    
    //判断是否需要释放资源
    old := new
    for {
    
    
        /**
         * 不需要唤醒的情况
         * 1.等待队列为0
         * 2.已经有协程抢到锁(上面的瞬间抢锁)
         * 3.已经有协程被唤醒
         */
        if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 {
    
    
            return
        }
        //将waiter计数位减一,并设置state为woken(唤醒)
        //问:会同时有多个被唤醒的协程存在吗
        new = (old - 1<<mutexWaiterShift) | mutexWoken
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
    
    
            runtime_Semrelease(&m.sema) // cas成功后,再做sema release操作,唤醒休眠的 goroutine
            return
        }
        old = m.state
    }
}

knowledge points

  1. Use & to judge the bit value, use | to set the bit value, and &^ to clear the position (memory alignment)

Generation mutex problem

  1. The priority of the dormant goroutine is lower than that of the currently active one, and the latest goroutine will grab the lock when the unlock is unlocked
  2. Most of the fruit locks are very short, and all goroutines have to sleep, which increases runtime scheduling overhead

2. Spinlock

There are three situations when the Lock method applies for locking the mutex

  1. No conflict Set the current state to the locked state through the CAS operation
  2. If there is a conflict, start spinning and wait for the lock to be released. If other goroutines release the lock during this time, they will directly acquire the lock; if not, go to 3
  3. There is a conflict By calling the semacquire function, the current goroutine enters the waiting state and wakes up when other coroutines release the lock
func (m *Mutex) Lock() {
    
    
    //快速加锁,逻辑不变
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    
    
        if race.Enabled {
    
    
            race.Acquire(unsafe.Pointer(m))
        }
        return
    }

    awoke := false
    iter := 0
    for {
    
    
        old := m.state
        new := old | mutexLocked
        if old&mutexLocked != 0 {
    
     // 如果当前己经上锁,那么判断是否可以自旋
            //短暂的自旋过后如果无果,就只能通过信号量让当前goroutine进入休眠等待了
            if runtime_canSpin(iter) {
    
    
                // Active spinning makes sense.
                /**
                 * 自旋的操作:设置state为woken,这样在unlock的时候就不会唤醒其他协程.
                 * 自旋的条件:
                 * 1.当前协程未被唤醒 !awoke
                 * 2.其他协程未被唤醒 old&mutexWoken == 0
                 * 3.等待队列大于0
                 */
                if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
                    atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
    
    
                    awoke = true
                }
                //进行自旋操作
                runtime_doSpin()
                iter++
                continue
            }
            new = old + 1<<mutexWaiterShit
        }
        if awoke {
    
    
            //todo 为什么加这个判断
            if new&mutexWoken == 0 {
    
    
                panic("sync: inconsistent mutex state")
            }
            new &^= mutexWoken
        }
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
    
    
            if old&mutexLocked == 0 {
    
    
                break
            }
            runtime_Semacquire(&m.sema)
            awoke = true
            iter = 0
        }
    }

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

path: runtime/proc.go

const (
        mutex_unlocked = 0
        mutex_locked   = 1
        mutex_sleeping = 2

        active_spin     = 4
        active_spin_cnt = 30
        passive_spin    = 1
)

/**
 * 有四种情况会返回false
 * 1.已经执行了很多次 iter >= active_spin 默认为4。避免长时间自旋浪费CPU
 * 2.是单核CPU ncpu <= 1 || GOMAXPROCS < 1 保证除了当前运行的Goroutine之外,还有其他的Goroutine在运行
 * 3.没有其他正在运行的p
 * 4 当前P的G队列为空 避免自旋锁等待的条件是由当前p的其他G来触发,这样会导致再自旋变得没有意义,因为条件永远无法触发
 */
func sync_runtime_canSpin(i int) bool {
    
    
        // sync.Mutex is cooperative, so we are conservative with spinning.
        // Spin only few times and only if running on a multicore machine and
        // GOMAXPROCS>1 and there is at least one other running P and local runq is empty.
        // As opposed to runtime mutex we don't do passive spinning here,
        // because there can be work on global runq or on other Ps.
        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
}

// 自旋逻辑
// procyeld函数内部循环调用PAUSE指令,PAUSE指令什么都不做,但是会消耗CPU时间
// 在这里会执行30次PAUSE指令消耗CPU时间等待锁的释放;
func sync_runtime_doSpin() {
    
    
    procyield(active_spin_cnt)
}

TEXT runtime·procyield(SB),NOSPLIT,$0-0
    MOVL    cycles+0(FP), AX
again:
    PAUSE
    SUBL    $1, AX
    JNZ again
    RET

Question :

  • Still hasn't solved the problem of low priority of sleeping process

3. Fair lock

basic logic

  1. Mutex has two working modes, normal mode and starvation mode. The logic of the lock in the normal case is similar to the old version. The dormant goroutine is stored in sudog in the form of a FIFO linked list. The awakened goroutine competes with the newly arrived active goroutine, but it is likely to fail. If a goroutine waits for more than 1ms, then Mutex enters starvation mode
  2. In starvation mode, after unlocking, the lock is directly handed over to the first one of the waiter FIFO linked list, and the new active goroutine does not participate in the competition, and is placed at the end of the FIFO queue
  3. If the goroutine currently acquiring the lock is at the end of the FIFO queue, or the waiting time is less than 1ms, exit starvation mode
  4. The performance in normal mode is better, but the starvation mode can reduce the long-tail latency

LOCK process :

  1. No conflict Set the current state to the locked state through the CAS operation
  2. If there is a conflict, start spinning. If it is starvation mode, spin is prohibited, start spinning, and wait for the lock to be released. If other goroutines release the lock during this time, directly acquire the lock; if not, go to 3
  3. There is a conflict, and the spin phase has passed. Call the semacquire function to put the current goroutine into the waiting state, and wake up when waiting for other coroutines to release the lock. Before sleeping: if it is in starvation mode, put the current coroutine at the front of the queue; After waking up: If it is awakened by starvation mode, directly acquire the lock
type Mutex struct {
    
    
        state int32 
        sema  **uint32**
}

// A Locker represents an object that can be locked and unlocked.
type Locker interface {
    
    
        Lock()
        Unlock()
}

//为什么使用位掩码表达式
//第3位到第32位表示等待在mutex上协程数量
const (
        mutexLocked = 1 << iota // mutex is locked 
        mutexWoken                                  
        mutexStarving           //新增饥饿状态
        mutexWaiterShift = iota                     
        starvationThresholdNs = 1e6 //饥饿状态的阈值:等待时间超过1ms就会进入饥饿状态
)


func (m *Mutex) Lock() {
    
    
        //快速加锁:逻辑不变
        if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    
    
                if race.Enabled {
    
    
                        race.Acquire(unsafe.Pointer(m))
                }
                return
        }

        var waitStartTime int64 //等待时间
        starving := false   //饥饿标记
        awoke := false      //唤醒标记
        iter := 0           //循环计数器
        old := m.state      //保存当前锁状态
        for {
    
    
            // 自旋的时候增加了一个判断:如果处于饥饿状态就不进入自旋,因为饥饿模式下,释放的锁会直接给等待队列的第一个,当前协程直接进入等待队列
                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
                }
                new := old
                // 当mutex不处于饥饿状态的时候,将new值设置为locked,也就是说如果是饥饿状态,新到来的goroutine直接排队
                if old&mutexStarving == 0 {
    
    
                        new |= mutexLocked
                }
                // 当mutex处于加锁锁或者饥饿状态时,新到来的goroutine进入等待队列
                if old&(mutexLocked|mutexStarving) != 0 {
    
    
                        new += 1 << mutexWaiterShift
                }
                // 当等待时间超过阈值,当前goroutine切换mutex为饥饿模式,如果未加锁,就不需要切换
                if starving && old&mutexLocked != 0 {
    
    
                        new |= mutexStarving
                }
                if awoke {
    
    
                        if new&mutexWoken == 0 {
    
    
                                throw("sync: inconsistent mutex state")
                        }
                        new &^= mutexWoken
                }
                if atomic.CompareAndSwapInt32(&m.state, old, new) {
    
    
                    // mutex 处于未加锁,正常模式下,当前 goroutine 获得锁
                        if old&(mutexLocked|mutexStarving) == 0 {
    
    
                                break // locked the mutex with CAS
                        }
                        // 如果已经在排队了,就排到队伍的最前面
                        queueLifo := waitStartTime != 0
                        if waitStartTime == 0 {
    
    
                                waitStartTime = runtime_nanotime()
                        }
                        // queueLifo 为真的时候,当前goroutine会被放到队头,
                        // 也就是说被唤醒却没抢到锁的goroutine放到最前面
                        runtime_SemacquireMutex(&m.sema, queueLifo)
                        // 当前goroutine等待时间超过阈值,切换为饥饿模式,starving设置为true
                        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")
                                }
                                // 如果切换为饥饿模式,等待队列计数减1
                                delta := int32(mutexLocked - 1<<mutexWaiterShift)
                                // 如果等待时间小于1ms或者自己是最后一个被唤醒的,退出饥饿模式
                                if !starving || old>>mutexWaiterShift == 1 {
    
    
                                        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 is unlocked in two steps

  1. Unlock, set the current state to unlock state through CAS operation
  2. To wake up the dormant coroutine, the CAS operation reduces the number of waiters in the current state by 1, and then wakes up the dormant goroutine.If it is hungry mode, wake up the first one in the waiting queue
func (m *Mutex) Unlock() {
    
    
        if race.Enabled {
    
    
                _ = m.state
                race.Release(unsafe.Pointer(m))
        }

        new := atomic.AddInt32(&m.state, -mutexLocked)
        if (new+mutexLocked)&mutexLocked == 0 {
    
    
                throw("sync: unlock of unlocked mutex")
        }

        if new&mutexStarving == 0 {
    
    
        // 正常模式
                old := new
                for {
    
    
                    /**
             * 不需要唤醒的情况
             * 1.等待队列为0
             * 2.已经有协程抢到锁(上面的瞬间抢锁)
             * 3.已经有协程被唤醒
             * 4.处于饥饿模式 在饥饿模式获取到锁的协程仍然处于饥饿状态,新的goroutine无法获取到锁
             */
                        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 {
    
    
            // 饥饿模式
                runtime_Semrelease(&m.sema, true)
        }
}

Guess you like

Origin blog.csdn.net/zhw21w/article/details/129488087