> ## 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.

# Go 1.24: Mutex Spin Optimization Significantly Enhances Performance
- URL: https://huizhou92.com/go-1-24-mutex-spin-optimization-significantly-enhances-performance/
- Published: 2025-02-11T10:39:36.000Z
- Updated: 2026-09-08T02:29:09.000Z
- Description: Go 1.24: Mutex Spin Optimization Significantly Enhances Performance. Background In 2024, Rhys Hiltner proposed performance optimizations for mutex locks。
- Author: huizhou92
- Tags: #Migrated-1788833207488, #Import 2026-09-08 02:07

### Background

In 2024, [Rhys Hiltner proposed performance optimizations for mutex locks](https://github.com/golang/go/issues/68578?ref=huizhou92.com). This optimization has now been merged into the upcoming Go 1.24 release, potentially enhancing performance by up to **70%** in scenarios with high lock contention.

![](https://huizhou92.com/content/images/2026/09/0-9bh-gl3jm_kwhw4o.png)

source [https://github.com/golang/go/issues/68578](https://github.com/golang/go/issues/68578?ref=huizhou92.com)

In the benchmark test ChanContended, the author observed a significant decline in mutex performance as `GOMAXPROCS` increased.  
**Intel i7-13700H** (linux/amd64):

- With 4 threads allowed, the overall throughput is half that of a single thread.
- With 8 threads allowed, the throughput is halved again.
- With 12 threads allowed, the throughput is halved once more.
- At `GOMAXPROCS=20`, 200 channel operations took an average of 44 microseconds, with an average of 220 nanoseconds per unlock2 call, each having the opportunity to wake a sleeping thread.

Another perspective is to consider the CPU usage time of the process. The following data shows that within 1.78 seconds of `Wall-Clock Time`, **the process's 20 threads spent 27.74 seconds in CPU (spinning) during lock2 calls.**

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

**These lock2-related threads did not sleep but continuously spun, consuming significant CPU resources.**

### New Proposal: Adding Spinning State

Through the analysis above, the author found that in the current lock2 implementation, although threads theoretically can sleep, they spin, leading to slower lock handoffs and considerable performance loss. Thus, a new design proposal was introduced [《Proposal: Improve scalability of runtime.lock2》](https://github.com/golang/proposal/blob/master/design/68578-mutex-spinbit.md?ref=huizhou92.com).

### Core Optimization Points

**A new flag called “spinning” has been added to the mutex state word.**

```go
https://github.com/golang/go/blob/608acff8479640b00c85371d91280b64f5ec9594/src/runtime/lock_spinbit.go#L57 
const ( 
    mutexLocked = 0x001 
    mutexSleeping = 0x002 
    mutexSpinning = 0x100 
    ... 
)
```

This `spinning` bit indicates whether a waiting thread is in a "awake and spinning" state. Threads will mutually exclude each other from entering the `spinning` state but will not block while attempting to acquire this flag.

> For an introduction to mutex, please refer to previous articles:

[Decrypt Go: sync.MutexUnderstand the sync.mutex from source code![](https://huizhou92.com/content/images/2026/09/0-j9ga4lzwfqru6mnl.png)](https://pub.huizhou92.com/go-source-code-sync-mutex-3082a25ef092?ref=huizhou92.com)

### Mutex Lock Acquisition Analysis

#### 1\. Fast Path Attempt to Acquire Lock

```go
//https://github.com/golang/go/blob/adc9c455873fef97c5759e4811f0d9c8217fe27b/src/runtime/lock_spinbit.go#L160 
k8 := key8(&l.key) 
v8 := atomic.Xchg8(k8, mutexLocked)  
if v8&mutexLocked == 0 { 
    if v8&mutexSleeping != 0 { 
        atomic.Or8(k8, mutexSleeping) 
    } 
    return 
}
```

The fast mode remains essentially unchanged from before. If successful (the lock was not previously held), it returns quickly. This is the ideal case: a fast path with no contention.

#### 2\. Spinning Wait Phase

```go
//https://github.com/golang/go/blob/adc9c455873fef97c5759e4811f0d9c8217fe27b/src/runtime/lock_spinbit.go#L208 
if !weSpin && v&mutexSpinning == 0 && atomic.Casuintptr(&l.key, v, v|mutexSpinning) { 
    v |= mutexSpinning  
    weSpin = true 
} 
if weSpin || atTail || mutexPreferLowLatency(l) { 
    if i < spin { 
        procyield(mutexActiveSpinSize)  //主动自旋 
        // ... 
    } else if i < spin+mutexPassiveSpinCount { 
        osyield() //被动自旋 
        // ... 
    } 
}
```

- If the fast path fails, it enters the spinning wait phase.
- The `mutexSpinning` flag controls **that only one goroutine is allowed to spin at a time**.
- Spinning is divided into `procyield` and `osyield`; the difference is that procyield continuously occupies the CPU, responding faster and suitable for very short wait times, while osyield temporarily releases the CPU, responding slower but consuming less CPU, ideal for longer wait times.  
This two-phase spinning design maintains good performance under varying contention levels.
- In light contention, active spinning is primarily used to ensure low latency.
- It quickly transitions to passive spinning to avoid wasting CPU resources in heavy contention.

### Sleeping Wait Phase

```go
//https://github.com/golang/go/blob/adc9c455873fef97c5759e4811f0d9c8217fe27b/src/runtime/lock_spinbit.go#L231 
// Store the current head of the list of sleeping Ms in our gp.m.mWaitList.next field 
gp.m.mWaitList.next = mutexWaitListHead(v) 
// Pack a (partial) pointer to this M with the current lock state bits 
next := (uintptr(unsafe.Pointer(gp.m)) &^ mutexMMask) | v&mutexMMask | mutexSleeping 
if weSpin { 
    next = next &^ mutexSpinning 
} 
if atomic.Casuintptr(&l.key, v, next) { 
    weSpin = false 
    semasleep(-1) 
    atTail = gp.m.mWaitList.next == 0 
    i = 0 
}
```

If spinning fails, the `goroutine` will enter a sleeping wait state, adding the current M to the wait queue (via the mWaitList linked list) and using a semaphore ([semasleep](https://github.com/golang/go/blob/fd050b3c6d0294b6d72adb014ec14b3e6bf4ad60/src/runtime/lock%5Fsema%5Ftristate.go?ref=huizhou92.com#L106)) to put the current `goroutine` to sleep, waiting for the `goroutine` holding of the lock to wake it upon unlocking.

When a thread unlocks the mutex, if it finds that a thread is already in the “awake and spinning” state, it will not wake other threads. In the context of Go runtime, this design is referred to as `spinbit`.  
The core purpose of this design is to **reduce contention and unnecessary thread switching by allowing one thread to be responsible for "spinning to acquire the lock," preventing all threads from competing for resources simultaneously.**

### Effects

```shell
goos: linux 
goarch: amd64 
pkg: runtime 
cpu: 13th Gen Intel(R) Core(TM) i7-13700H 
                │     old     │                  new                  │ 
                │   sec/op    │    sec/op     vs base                 │ 
ChanContended      3.147µ ± 0%    3.703µ ± 0%   +17.65% (p=0.000 n=10) 
ChanContended-2    4.511µ ± 2%    5.280µ ± 7%   +17.06% (p=0.000 n=10) 
ChanContended-3    5.726µ ± 2%   12.125µ ± 2%  +111.75% (p=0.000 n=10) 
ChanContended-4    6.574µ ± 1%   13.356µ ± 4%  +103.16% (p=0.000 n=10) 
ChanContended-5    7.706µ ± 1%   13.717µ ± 3%   +78.00% (p=0.000 n=10) 
ChanContended-6    8.830µ ± 1%   13.674µ ± 2%   +54.85% (p=0.000 n=10) 
ChanContended-7    11.07µ ± 0%    13.59µ ± 2%   +22.77% (p=0.000 n=10) 
ChanContended-8    13.99µ ± 1%    14.06µ ± 1%         ~ (p=0.190 n=10) 
ChanContended-9    16.93µ ± 2%    14.04µ ± 3%   -17.04% (p=0.000 n=10) 
ChanContended-10   20.12µ ± 4%    14.12µ ± 1%   -29.80% (p=0.000 n=10) 
ChanContended-11   23.96µ ± 2%    14.44µ ± 3%   -39.74% (p=0.000 n=10) 
ChanContended-12   29.65µ ± 6%    14.61µ ± 3%   -50.74% (p=0.000 n=10) 
ChanContended-13   33.98µ ± 7%    14.69µ ± 3%   -56.76% (p=0.000 n=10) 
ChanContended-14   37.90µ ± 1%    14.69µ ± 3%   -61.23% (p=0.000 n=10) 
ChanContended-15   37.94µ ± 4%    14.89µ ± 5%   -60.75% (p=0.000 n=10) 
ChanContended-16   39.56µ ± 0%    13.89µ ± 1%   -64.89% (p=0.000 n=10) 
ChanContended-17   39.56µ ± 0%    14.45µ ± 4%   -63.47% (p=0.000 n=10) 
ChanContended-18   41.24µ ± 2%    13.95µ ± 3%   -66.17% (p=0.000 n=10) 
ChanContended-19   42.77µ ± 5%    13.80µ ± 2%   -67.74% (p=0.000 n=10) 
ChanContended-20   44.26µ ± 2%    13.74µ ± 1%   -68.96% (p=0.000 n=10) 
geomean            17.60µ         12.46µ        -29.22%
```

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

source [https://github.com/golang/go/issues/68578#issuecomment-2256792628](https://github.com/golang/go/issues/68578?ref=huizhou92.com#issuecomment-2256792628)

Although performance decreases under light contention, it significantly improves under heavy contention. On average, a performance enhancement of approximately **29%** is achieved. We look forward to further optimizations in this area.

This modification to the mutex does not involve changes at the API level so that it will be automatically utilized upon the official release of Go 1.24\. This feature can be controlled via `GOEXPERIMENT=spinbitmutex`, enabled by default but disabled to use the original Mutex."