当前位置:网站首页>Important knowledge of golang: mutex

Important knowledge of golang: mutex

2022-06-23 15:24:00 yue_ xin_ tech

Abstract

Go It is said to be born for high concurrency , In high concurrency scenarios , It is bound to involve competition for public resources . When the corresponding scene occurs , We often use mutex Of Lock() and Unlock() Methods to occupy or release resources . Although the call is simple , but mutex But it involves a lot of . today , Let's study it carefully .

mutex A preliminary understanding

mutex The source code is mainly in src/sync/mutex.go In the document , Its structure is relatively simple , as follows :

type Mutex struct {
    
	state int32
	sema  uint32
}

We can see that there is a field sema, It represents the semaphore marker bit . The so-called semaphore is used for Goroutine Blocked or awakened between . This is a bit like the operating system PV The original language operation , Let's meet first PV Primitive operations :

PV The original language explains :
By operating semaphores S To deal with the synchronization and mutual exclusion between processes .
S>0: Express S Resources available ;S=0 No resources are available ;S<0 The absolute number of processes or queues in a linked list . Semaphore S The initial value of should be greater than or equal to 0.
P The original language : Apply for a resource , Yes S Atomic subtraction 1, if reduce 1 Still after S>=0, Then the process continues ; if reduce 1 after S<0, Indicates that no resources are available , You need to block yourself up , Put it in the waiting queue .
V The original language : To release a resource , Yes S Atomic addition 1; if Add 1 after S>0, Then the process continues ; if Add 1 after S<=0, Indicates that there are waiting processes on the waiting queue , Need to wake up the first waiting process .

Through the above explanation ,mutex You can use semaphores to realize goroutine Blocking and arousing .

Actually mutex In essence, it is about Semaphore Of Blocking arousal operation .

When goroutine When the lock resource cannot be occupied, it will be blocked and suspended , The following code logic cannot be executed at this time .

When mutex When the lock resource is released , Will continue to evoke the previous goroutine To preempt lock resources .

as for mutex Of state The status field is used for status flow , These state values involve some concepts , Let's explain it in detail .

mutex Status flags

mutex Of state Yes 32 position , It's low 3 Bits denote respectively 3 States : Wake up state Locked state Starvation , The remaining bits represent the number of currently blocked waits goroutine Number .

mutex According to the current state State to enter Normal mode Hunger mode Or is it The spin .

mutex Normal mode

When mutex call Unlock() Method to release the lock resource , If you find something waiting to be aroused Goroutine When queuing , Will lead the team Goroutine evoke .

Team leader goroutine After being aroused , Would call CAS Methods to try to modify state state , If the modification is successful , It means that the lock resource is occupied successfully .

( notes :CAS stay Go In the use atomic.CompareAndSwapInt32(addr *int32, old, new int32) Method realization ,CAS Similar to the optimistic locking effect , Before modifying, you will first determine whether the address value is still old value , Only or old value , Will continue to be modified to new value , Otherwise it will return to false Indicates that the modification failed .)

mutex Hunger mode

Because of the above Goroutine Arousal does not directly occupy resources , You also need to call CAS The way to Try Occupy lock resources . If there are new arrivals Goroutine, Then it will also call CAS Methods to try to occupy resources .

But for the Go In terms of the scheduling mechanism , Will be more inclined to CPU Occupying for a short time Goroutine First run , And this will create a certain probability that the newcomers Goroutine All the way to the lock resource , At this time, the leader of the team Goroutine Will not occupy until , Lead to Starve to death .

In this case ,Go The hunger model is adopted . That is, by judging the team leader Goroutine When resources are still unavailable after a certain period of time , Will be in Unlock When the lock resource is released , Directly give the lock resource to the team leader Goroutine, And change the current status to Hunger mode .

If there is a newcomer in the back Goroutine When it turns out to be a hunger pattern , Will be added directly to the end of the waiting queue .

mutex The spin

If Goroutine The lock resource is occupied for a short time , Then call semaphores every time to block the call goroutine, Will be very waste resources .

So after meeting certain conditions ,mutex Will make the current Goroutine Go to Idle CPU, Call again after idling CAS Methods to try to occupy lock resources , Until the spin condition is not met , Will eventually be added to the waiting queue .

The spin conditions are as follows :

  • Not spinning more than 4 Time
  • Multicore processor
  • GOMAXPROCS > 1
  • p Local Goroutine The queue is empty

It can be seen that , The spin condition is quite strict , After all, it consumes CPU Computing power of .

mutex Of Lock() The process

First , If mutex Of state = 0, That is, no one is occupying resources , There is no block waiting to be aroused goroutine. It will call CAS Method to try to possess the lock , Don't do anything else .

If it doesn't meet m.state = 0, Then it is further judged whether it is necessary to spin .

When you don't need to spin, or you still can't get resources after spinning , This will call runtime_SemacquireMutex Semaphore function , Change the current goroutine Block and join the waiting queue .

When a lock resource is released ,mutex In arousing the head of the team goroutine after , Team head goroutine Will try to occupy lock resources , At this time, it may also be with the new goroutine Compete together .

Be the head of the team goroutine When resources are not available , Will go into starvation mode , Directly give the lock resource to the team leader goroutine, Let the newcomers goroutine Block and join the tail of the waiting queue .

The starvation mode will continue until there is no blocking waiting to be aroused goroutine When queuing , It's going to lift .

Unlock The process

mutex Of Unlock() It's relatively simple . alike , It will unlock quickly first , That is, there is no waiting to be aroused goroutine, There is no need to continue to do other actions .

If the current mode is normal , Is simple evoke Team head Goroutine. If it's hunger mode , will direct Give the lock to the team leader Goroutine, Then arouse the team leader Goroutine, Let it continue to run .

mutex Code details

Okay , The above general process has been completed , The detailed code flow will be presented below , Let us know in more detail mutex Of Lock()、Unlock() Method logic .

mutex Lock() Code details :


// Lock mutex  Method of locking .
func (m *Mutex) Lock() {
    
	//  Quick lock .
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    
		if race.Enabled {
    
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	//  Quick lock failed , The locking action with more operations will be carried out .
	m.lockSlow()
}

func (m *Mutex) lockSlow() {
    
  var waitStartTime int64  //  Record the current  goroutine  The waiting time of 
  starving := false //  Whether you are hungry 
  awoke := false //  Wake up or not 
  iter := 0 //  Number of spins 
  old := m.state //  At present  mutex  The state of 
  for {
    
    //  At present  mutex  Is locked , And non starvation mode , And it meets the spin condition 
    if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
    
      //  The wake-up flag has not been set yet 
      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
    //  If it's not hunger , Then try locking 
    //  If it's starvation , Will not lock , Because of the current  goroutine  Will be blocked and added to the end of the queue waiting to be recalled 
    if old&mutexStarving == 0 {
    
      new |= mutexLocked
    }
    //  Number of waiting queues  + 1
    if old&(mutexLocked|mutexStarving) != 0 {
    
      new += 1 << mutexWaiterShift
    }
    //  If  goroutine  It used to be hunger mode , It is also set to hunger mode this time 
    if starving && old&mutexLocked != 0 {
    
      new |= mutexStarving
    }
    //
    if awoke {
    
      //  If the status is not as expected , False report 
      if new&mutexWoken == 0 {
    
        throw("sync: inconsistent mutex state")
      }
      //  The new status value needs to clear the wake-up flag , Because the current  goroutine  Will lock or re  sleep
      new &^= mutexWoken
    }
    // CAS  Try to modify the status , If the modification is successful, the lock resource is obtained 
    if atomic.CompareAndSwapInt32(&m.state, old, new) {
    
      //  Non starvation mode , And the lock has not been acquired , It means that the lock acquired this time is  ok  Of , direct  return
      if old&(mutexLocked|mutexStarving) == 0 {
    
        break
      }
      //  Calculate according to the waiting time  queueLifo
      queueLifo := waitStartTime != 0
      if waitStartTime == 0 {
    
        waitStartTime = runtime_nanotime()
      }
      //  Come here , Indicates that the lock is not successful 
      // queueLife = true,  It will put  goroutine  Put it at the head of the waiting queue 
      // queueLife = false,  It will put  goroutine  Put it at the end of the waiting queue 
      runtime_SemacquireMutex(&m.sema, queueLifo, 1)
      //  Whether the calculation conforms to the hunger model , That is, whether the waiting time exceeds a certain time 
      starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
      old = m.state
      //  Last time it was hunger mode 
      if old&mutexStarving != 0 {
    
        if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
    
          throw("sync: inconsistent mutex state")
        }
        delta := int32(mutexLocked - 1<<mutexWaiterShift)
        //  This time it's not hunger mode, or the next time there's no waiting queue to wake up  goroutine  了 
        if !starving || old>>mutexWaiterShift == 1 {
    
          delta -= mutexStarving
        }
        atomic.AddInt32(&m.state, delta)
        break
      }
      //  This is no longer hunger mode , Clear spin count , Back to  for  Circular contention lock .
      awoke = true
      iter = 0
    } else {
    
      old = m.state
    }
  }if race.Enabled {
    
    race.Acquire(unsafe.Pointer(m))
  }
}

mutex Unlock() Code details :

// Unlock  Yes  mutex  Unlock .
//  If it hasn't been locked , Call this method to unlock , Will throw a runtime error .
//  It will allow in different  Goroutine  Lock and unlock on the 
func (m *Mutex) Unlock() {
    
	if race.Enabled {
    
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	//  Quick try to unlock 
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {
    
		//  Quick unlock failed , The unlocking action with more operations will be performed .
		m.unlockSlow(new)
	}
}

func (m *Mutex) unlockSlow(new int32) {
    
  //  Unlocked state , Throw an exception directly 
  if (new+mutexLocked)&mutexLocked == 0 {
    
    throw("sync: unlock of unlocked mutex")
  }
  //  Normal mode 
  if new&mutexStarving == 0 {
    
    old := new
    for {
    
      //  There is no waiting queue to be invoked 
      if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
    
        return
      }
      //  Recall the waiting queue and count -1
      new = (old - 1<<mutexWaiterShift) | mutexWoken
      if atomic.CompareAndSwapInt32(&m.state, old, new) {
    
        runtime_Semrelease(&m.sema, false, 1)
        return
      }
      old = m.state
    }
  } else {
    
    // Hunger mode , Give the lock directly to the head of the waiting queue  goroutine
    runtime_Semrelease(&m.sema, true, 1)
  }
}

Interested friends can search the official account 「 Read new technology 」, Pay attention to more push articles .
If you can , Just like it by the way 、 Leave a message 、 Under the share , Thank you for your support !
Read new technology , Read more new knowledge .
 Read new technology

原网站

版权声明
本文为[yue_ xin_ tech]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/174/202206231438381407.html