golang read-write lock

The go language provides an out-of-the-box way to share resources. Mutex (sync.Mutex), the zero value of sync.Mutex indicates that it is not locked and can be used directly. After a goroutine acquires the mutex Other goroutines can only wait until the goroutine releases the mutex. Only two functions are exposed in the Mutex structure, namely Lock and Unlock. It is very simple to use the mutex, and this article does not describe the use.

Never make a value copy when using sync.Mutex, as this may invalidate the lock. When we open our IDE and jump into our sync.Mutex code we will find it has the following structure:

type Mutex struct {
 state int32 //The mutex lock state enumeration value is as follows
 sema uint32 //Semaphore, send a signal to G in Gwaitting
}
 
const (
 mutexLocked = 1 << iota // 1 mutex is locked
 mutexWoken // 2 wake lock
 mutexWaiterShift = iota // 2 counts the number of goroutines blocked on this mutex and needs to be shifted
)

The above state values ​​are 0 (available) 1 (locked) 2~31 waiting for the queue count

The following is the source code of the mutex. There are four important methods that need to be explained in advance, namely runtime_canSpin, runtime_doSpin, runtime_SemacquireMutex, runtime_Semrelease,

1. Runtime_canSpin: a relatively conservative spin. The spin lock in golang does not spin all the time. The runtime_canSpin method in the runtime package has some restrictions. The passed iter is greater than or equal to 4 or the number of cpu cores is less than or equal to 1, and the maximum The logical processor is greater than 1, there is at least a local P queue, and the local P queue can run the G queue is empty.

//go:linkname sync_runtime_canSpin sync.runtime_canSpin
func sync_runtime_canSpin(i int) bool {
 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
}

2. runtime_doSpin: will call the procyield function (give out the thread), which is also implemented in assembly language. The function inner loop calls the PAUSE instruction. The PAUSE instruction does nothing, but consumes CPU time, and the CPU does not unnecessarily optimize the PAUSE instruction when it is executed.

//go:linkname sync_runtime_doSpin sync.runtime_doSpin
func sync_runtime_doSpin() {
 procyield(active_spin_cnt)
}

3、runtime_SemacquireMutex:

//go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex
func sync_runtime_SemacquireMutex(addr *uint32) {
 semacquire(addr, semaBlockProfile|semaMutexProfile)
}

4、runtime_Semrelease:

//go:linkname sync_runtime_Semrelease sync.runtime_Semrelease
func sync_runtime_Semrelease(addr *uint32) {
	semrelease(addr)
}

  

The Lock function of Mutex is defined as follows

func (m *Mutex) Lock() { 
//Use CAS to try to acquire the lock first
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
//This is -race, don't
care if race.Enabled {
race.Acquire (unsafe.Pointer(m))
}
//successful acquisition return
return
}

awoke := false //loop mark
iter := 0 //loop counter
for {
old := m.state //get current lock state
new := old | mutexLocked //Assign the last bit of the current state to 1
if old&mutexLocked != 0 { //If the lock is occupied
if runtime_canSpin(iter) { //Check if the spin lock can be entered
if !awoke && old&mutexWoken == 0 && old> >mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true //awoke标记为true
}
                //Enter the spin state 
runtime_doSpin()
iter++
continue
}
//The lock is not acquired, the current G enters the Gwaitting state, and the waiting sequence adds 1
new = old + 1<<mutexWaiterShift
}
if awoke {
if new&mutexWoken == 0 {
throw( "sync: inconsistent mutex state")
}
//clear flag
new &^= mutexWoken
}
//update state
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&mutexLocked == 0 {
break
}

// lock request failed , enter the sleep state, wait for the signal to wake up and restart the cycle
runtime_SemacquireMutex(&m.sema)
awoke = true
iter = 0
}
}

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

Mutex's Unlock function is defined as follows

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

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

    old := new
    for {
    //When the waiting count in the sleep queue is 0 or the spin state counter is 0, exit
        if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 {
            return
        }
        // Reduce the number of waits and add a clear flag
        new = (old - 1<<mutexWaiterShift) | mutexWoken
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // Release the lock and send the release signal
            runtime_Semrelease(&m.sema)
            return
        }
        old = m.state
    }
}

Mutex-free conflict is the simplest case. When there is a conflict, spin first, because most of the code segments protected by Mutex are very short and can be obtained after a short spin; if the spin wait is fruitless, It is only through the semaphore to let the current Goroutine enter the Gwaitting state.

The above is the whole content of this article, I hope it will be helpful to everyone's learning, and I hope everyone will support Scripting Home.

 

Reprinted from https://zhuanlan.zhihu.com/p/27608263

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325070650&siteId=291194637