> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Decrypt Go: sync.Mutex
- URL: https://huizhou92.com/go-source-code-sync-mutex/
- Published: 2024-02-19T13:24:30.000Z
- Updated: 2026-09-08T02:39:00.000Z
- Description: Decrypt Go: sync.Mutex. Background sync.Mutex is a lock that we commonly use. Many articles about this lock online, but I will summarize them here for pers。
- Author: huizhou92
- Tags: #Migrated-1788833207488, #Import 2026-09-08 02:07

### Background

`sync.Mutex` is a lock that we commonly use. Many articles about this lock online, but I will summarize them here for personal reference.

The slow path of `Sync.Mutex` relies on `runtime_SemacquireMutex` and `runtime_Semrelease`. If you are unfamiliar with them, you can first read about `runtime.semaphore`

### Sync.Mutex source code

### Development History

The initial version of `sync.Mutex` [code](https://github.com/golang/go/commit/bf3dd3f0efe5b45947a991e22660c62d4ce6b671?ref=huizhou92.com#diff-a8c424f9dc7e3acf3f180a5cbf3f7748e6fd39c6f1eab0b4fd7ec11c548cdbeb) was submitted by @rsc in 2008\. The early implementation was relatively simple, using a combination of CAS (Compare-And-Swap) and semaphores. You can refer to the article [runtime-sema](https://fanlv.fun/2022/10/06/runtime-sema/?ref=huizhou92.com) for more details.

In 2011, @dvyukov submitted the first optimization [sync: improve Mutex to allow successive acquisitions](https://codereview.appspot.com/4631075/?ref=huizhou92.com), which introduced the concepts of `mutexWoken` (wakeup status) and waiter count.

In 2015, @dvyukov made the second optimization [sync: add active spinning to Mutex](https://go-review.googlesource.com/c/go/+/5430/?ref=huizhou92.com), which mainly added spinning logic.

In 2016, @dvyukov made the third optimization [sync: make Mutex more fair](https://go-review.googlesource.com/c/go/+/34310/?ref=huizhou92.com), which introduced the concept of starvation mode to make the lock more fair.

### Mutex Structure Analysis

Let's first look at the [comment](https://github.com/golang/go/blob/release-branch.go1.18/src/sync/mutex.go?ref=huizhou92.com#L42) of `Mutex`:

```less
// Mutex fairness. 
 // 
 // Mutex can be in 2 modes of operations: normal and starvation. 
 // In normal mode waiters are queued in FIFO order, but a woken up waiter 
 // does not own the mutex and competes with new arriving goroutines over 
 // the ownership. New arriving goroutines have an advantage -- they are 
 // already running on CPU and there can be lots of them, so a woken up 
 // waiter has good chances of losing. In such case it is queued at front 
 // of the wait queue. If a waiter fails to acquire the mutex for more than 1ms, 
 // it switches mutex to the starvation mode. 
 // 
 // In starvation mode ownership of the mutex is directly handed off from 
 // the unlocking goroutine to the waiter at the front of the queue. 
 // New arriving goroutines don't try to acquire the mutex even if it appears 
 // to be unlocked, and don't try to spin. Instead they queue themselves at 
 // the tail of the wait queue. 
 // 
 // If a waiter receives ownership of the mutex and sees that either 
 // (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms, 
 // it switches mutex back to normal operation mode. 
 // 
 // Normal mode has considerably better performance as a goroutine can acquire 
 // a mutex several times in a row even if there are blocked waiters. 
 // Starvation mode is important to prevent pathological cases of tail latency.
```

Now, let's take a look at the [Mutex](https://github.com/golang/go/blob/release-branch.go1.18/src/sync/mutex.go?ref=huizhou92.com#L21) structure:

```go
type Mutex struct { 
     state int32 
     sema  uint32 
 } 
 ​ 
 const ( 
     mutexLocked = 1 << iota // Indicates whether the mutex is locked: 1 for locked, 0 for unlocked 
     mutexWoken             // Indicates whether it is in wakeup status: 1 for woken up 
     mutexStarving          // Indicates whether it is in starvation mode: 1 for starving 
     mutexWaiterShift = iota // Shifts the state to represent the number of waiters 
     starvationThresholdNs = 1e6 // If the wait time exceeds this value, it switches to starvation mode 
 )
```

The `sema` field is relatively simple; it is the parameter required for `runtime_SemacquireMutex` and `runtime_Semrelease` calls. The `state` field represents different meanings based on different bits, as shown in the following diagram:

![](https://huizhou92.com/content/images/2026/09/0-uoj1st69_6nzp39q-png.jpg)

### Lock

```go
// If the mutex is already locked, this will block the current goroutine until the mutex is available. 
 func (m *Mutex) Lock() { 
     // Fast path: Try CAS to change the state from 0 to locked. 
     if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { 
         if race.Enabled { 
             race.Acquire(unsafe.Pointer(m)) 
         } 
         return 
     } 
 ​ 
     // Slow path 
     m.lockSlow() 
 } 
 ​ 
 func (m *Mutex) lockSlow() { 
     var waitStartTime int64 
     starving := false 
     awoke := false 
     iter := 0 
     old := m.state 
     for { 
         // old&(mutexLocked|mutexStarving) preserves the data on the Locked and Starving bits and clears the rest. 
         // old&(mutexLocked|mutexStarving) == mutexLocked indicates a locked state without starvation. 
         // runtime_canSpin checks if spinning is possible and performs the following checks: 
         // 1. Spin count < 4 
         // 2. Must be a multicore CPU with GOMAXPROCS > 1 
         // 3. P and the local run queue are empty. 
         if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { 
             // If the "woken" flag is 0 and there are other goroutines waiting, 
             // try to set the "woken" flag to 1 through CAS. 
             // This tells other goroutines that we are currently spinning to acquire the lock. 
             if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && 
                 atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { 
                 awoke = true 
             } 
             runtime_doSpin() 
             iter++ 
             old = m.state // Read the new value of m.state, which may have been changed by other goroutines. 
             continue // Retry spinning if the setting fails 
         } 
 ​ 
         new := old 
 ​ 
         if old&mutexStarving == 0 { 
             // If it is not in starvation mode, try to lock it. 
             // If it is in starvation mode, no need to set it again. The waiter count will be increased, and it will obediently wait in line. 
             new |= mutexLocked 
         } 
 ​ 
         // If mutexLocked or mutexStarving = 1 
         // Increase the waiter count. 
         if old&(mutexLocked|mutexStarving) != 0 { 
             new += 1 << mutexWaiterShift 
         } 
 ​ 
         // If it is currently mutexLocked = 1 (locked state) 
         // and starving = true (waiting time exceeds 1ms) 
         // Set mutexStarving to 1. 
         // If it is not in a locked state, we don't need to set it to starvation mode. It might succeed in the following CAS operation. 
         if starving && old&mutexLocked != 0 { 
             new |= mutexStarving 
         } 
         if awoke { 
             // If it has already been set to wakeup status, clear the wakeup flag, as we will either acquire the lock or go to sleep. 
             if new&mutexWoken == 0 { 
                 throw("sync: inconsistent mutex state") 
             } 
             new &^= mutexWoken 
         } 
 ​ 
         // CAS update the state 
         if atomic.CompareAndSwapInt32(&m.state, old, new) { 
             if old&(mutexLocked|mutexStarving) == 0 { 
                 // If the previous state was not locked or starving, it means we have successfully acquired the lock. 
                 // Return directly. 
                 break // Locked the mutex with CAS 
             } 
 ​ 
             // If it reaches here, it means the previous state might be locked or in starvation mode. 
             // Regardless of whether it is locked or in starvation mode, we need to call the semaphore to wait in line. 
 ​ 
             // If waitStartTime != 0, it means that this goroutine was awakened after sleep, and queueLifo = true. 
             // If queueLifo = true, it will be placed at the head of the semTable suodg queue. 
             // You can refer to this [article](https://fanlv.fun/2022/10/06/runtime-sema/) for more information about semaphores. 
             // If there are no available semaphores, it will block at this line. It actually calls gopark to put this goroutine to sleep. 
             runtime_SemacquireMutex(&m.sema, queueLifo, 1) 
 ​ 
             // This line means that someone has released the lock/semaphore, and this goroutine has been awakened. 
             // Although we are awakened at the head of the queue, if there is a new Lock call in the business code at this time, 
             // our awakened goroutine will compete for the lock in the new Lock scenario. 
             // If the wait time exceeds 1ms, set starving = true. 
             starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs 
             old = m.state // Read the latest state. We don't know what it has been changed to. 
             if old&mutexStarving != 0 { 
                 // If it is currently in starvation mode, we don't need to compete for the lock anymore; it will be given to us by default. 
                 if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { 
                     // It is impossible to have (mutexWoken=0 && mutexLocked==0) in starvation mode. 
                     // mutexWaiter cannot be 0 either because it will exit starvation mode when mutexWaiter = 1. 
                     throw("sync: inconsistent mutex state") 
                 } 
 ​ 
                 // The following bit operation changes three flag states with one AddInt32, which is complicated and hard to understand. 
                 // Set the first bit to 1 and decrease waiter by 1. 
                 // mutexLocked = 1, mutexWaiterShift = 3, delta = -7 
                 // The third bit of delta is 11111 0 0 1. 
                 delta := int32(mutexLocked - 1<<mutexWaiterShift) 
                 if !starving || old>>mutexWaiterShift == 1 { 
                     // If there is no more waiting, it should exit. 
                     delta -= mutexStarving 
                 } 
                 // Modify the state. 
                 atomic.AddInt32(&m.state, delta) 
                 break 
             } 
             awoke = true 
             iter = 0 
         } else { 
             // atomic.CompareAndSwapInt32(&m.state, old, new) 
             // If CAS fails, read the current state again and loop again. 
             old = m.state 
         } 
     } 
 ​ 
     if race.Enabled { 
         race.Acquire(unsafe.Pointer(m)) 
     } 
 }
```

### unlock

```go
func (m *Mutex) Unlock() { 
     if race.Enabled { 
         _ = m.state 
         race.Release(unsafe.Pointer(m)) 
     } 
 ​ 
     // Fast path: CAS cancels the lock if it is unlocked, 0 means no other lock waiters 
     // If unsuccessful, enter the slow path 
     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 { 
         // new = m.state-mutexLocked 
         // m.state&mutexLocked == 0 indicates an unlocked state. 
         // If it's unlocked, the fast path above succeeded. 
         // So theoretically, this case should not occur. 
         fatal("sync: unlock of unlocked mutex") 
     } 
     if new&mutexStarving == 0 { // Not in a starving state 
         old := new 
         for { 
             // If there are no waiters for the lock, or if any of the following conditions have already occurred, 
             // the subsequent work is unnecessary, so return directly: 
             // 1. The lock is in a locked state, indicating that it has been acquired by another goroutine. 
             // 2. The lock is in a woken state, indicating that a waiting goroutine has been woken up, so there's no need to try waking up other goroutines. 
             // 3. The lock is in a starving mode, in which case the lock will be directly handed over to the goroutine at the head of the waiting queue. 
             if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { 
                 return 
             } 
 ​ 
             // If the lock is currently idle and there are waiters in the queue, and no goroutines have been woken up, 
             // decrement the waiter count by 1 and set the woken state to 1. 
             new = (old - 1<<mutexWaiterShift) | mutexWoken 
             if atomic.CompareAndSwapInt32(&m.state, old, new) { // If successful in setting the state 
                 runtime_Semrelease(&m.sema, false, 1) // Wake up one semaphore 
                 return 
             } 
             old = m.state // Check the latest state again 
         } 
     } else { 
         // In the starving mode, wake up the head of the semaphore's waiting queue. 
         // Goroutines coming from the starving state will be placed at the end of the semaphore queue. 
         runtime_Semrelease(&m.sema, true, 1) 
     } 
 }
```

An optimization is made in the starving mode, where `readyWithTime` is called to put the head goroutine of the queue into `pp.runnext`. Then `goyield` is called to put the current goroutine into the tail of the `p` runnable queue. Finally, the `schedule` function is called, allowing the execution of goroutines in the waiting queue to be prioritized.

For more details, please refer to this CR: sync: yield to the waiter when unlocking a starving mutex

### Summary

To understand `Sync.Mutex`, it is important to first understand `runtime.semaphore`, and then comprehend the normal and starving modes based on the comments.

Designing a bug-free and high-performance lock is challenging without a certain level of technical depth. Understanding is one thing, but there is still a long way to go before implementing a lock oneself.

If you found my article enjoyable, feel free to follow me and give it a 👏. Your support would be greatly appreciated.

[文章索引](/article-index/)