Go 1.24: Mutex Spin Optimization Significantly Enhances Performance
An overview of the mutex spin optimization introduced in Go 1.24 and its impact on performance.
Background
In 2024, Rhys Hiltner proposed performance optimizations for mutex locks. 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.

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.

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》.
Core Optimization Points
A new flag called “spinning” has been added to the mutex state word.
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:

Mutex Lock Acquisition Analysis
1. Fast Path Attempt to Acquire Lock
//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
//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
mutexSpinningflag controls that only one goroutine is allowed to spin at a time. - Spinning is divided into
procyieldandosyield; 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
//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) 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
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%
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."