aboutsummaryrefslogtreecommitdiff
path: root/src/sync
diff options
context:
space:
mode:
authorMichael Anthony Knyszek <mknyszek@google.com>2024-06-21 17:01:23 +0000
committerGopher Robot <gobot@golang.org>2024-11-18 19:57:35 +0000
commit6c660052856feae2bf1f3fe44665b5da0002500d (patch)
treee6a6352be92a049654911bff35fc9c9ce0ce462c /src/sync
parentb5906ac4b7b455714565fa0a8f53aea966f3fb6f (diff)
downloadgo-6c660052856feae2bf1f3fe44665b5da0002500d.tar.xz
internal/sync: move sync.Mutex implementation into new package
This CL refactors sync.Mutex such that its implementation lives in the new internal/sync package. The purpose of this change is to eventually reverse the dependency edge between internal/concurrent and sync, such that sync can depend on internal/concurrent (or really, its contents, which will likely end up in internal/sync). The only change made to the sync.Mutex code is the frame skip count for mutex profiling, so that the internal/sync frames are omitted in the profile. Change-Id: Ib3603d30e8e71508c4ea883a584ae2e51ce40c3f Reviewed-on: https://go-review.googlesource.com/c/go/+/594056 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: David Chase <drchase@google.com> Auto-Submit: Michael Knyszek <mknyszek@google.com>
Diffstat (limited to 'src/sync')
-rw-r--r--src/sync/mutex.go207
-rw-r--r--src/sync/runtime.go11
-rw-r--r--src/sync/rwmutex.go12
3 files changed, 13 insertions, 217 deletions
diff --git a/src/sync/mutex.go b/src/sync/mutex.go
index cd50fcbbb5..133c9530fd 100644
--- a/src/sync/mutex.go
+++ b/src/sync/mutex.go
@@ -11,15 +11,9 @@
package sync
import (
- "internal/race"
- "sync/atomic"
- "unsafe"
+ isync "internal/sync"
)
-// Provided by runtime via linkname.
-func throw(string)
-func fatal(string)
-
// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
@@ -36,8 +30,7 @@ func fatal(string)
type Mutex struct {
_ noCopy
- state int32
- sema uint32
+ mu isync.Mutex
}
// A Locker represents an object that can be locked and unlocked.
@@ -46,52 +39,11 @@ type Locker interface {
Unlock()
}
-const (
- mutexLocked = 1 << iota // mutex is locked
- mutexWoken
- mutexStarving
- mutexWaiterShift = iota
-
- // 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.
- starvationThresholdNs = 1e6
-)
-
// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
func (m *Mutex) Lock() {
- // Fast path: grab unlocked mutex.
- if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
- if race.Enabled {
- race.Acquire(unsafe.Pointer(m))
- }
- return
- }
- // Slow path (outlined so that the fast path can be inlined)
- m.lockSlow()
+ m.mu.Lock()
}
// TryLock tries to lock m and reports whether it succeeded.
@@ -100,111 +52,7 @@ func (m *Mutex) Lock() {
// and use of TryLock is often a sign of a deeper problem
// in a particular use of mutexes.
func (m *Mutex) TryLock() bool {
- old := m.state
- if old&(mutexLocked|mutexStarving) != 0 {
- return false
- }
-
- // There may be a goroutine waiting for the mutex, but we are
- // running now and can try to grab the mutex before that
- // goroutine wakes up.
- if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) {
- return false
- }
-
- if race.Enabled {
- race.Acquire(unsafe.Pointer(m))
- }
- return true
-}
-
-func (m *Mutex) lockSlow() {
- var waitStartTime int64
- starving := false
- awoke := false
- iter := 0
- old := m.state
- for {
- // Don't spin in starvation mode, ownership is handed off to waiters
- // so we won't be able to acquire the mutex anyway.
- if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
- // Active spinning makes sense.
- // Try to set mutexWoken flag to inform Unlock
- // to not wake other blocked goroutines.
- 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
- // Don't try to acquire starving mutex, new arriving goroutines must queue.
- if old&mutexStarving == 0 {
- new |= mutexLocked
- }
- if old&(mutexLocked|mutexStarving) != 0 {
- new += 1 << mutexWaiterShift
- }
- // The current goroutine switches mutex to starvation mode.
- // But if the mutex is currently unlocked, don't do the switch.
- // Unlock expects that starving mutex has waiters, which will not
- // be true in this case.
- if starving && old&mutexLocked != 0 {
- new |= mutexStarving
- }
- if awoke {
- // The goroutine has been woken from sleep,
- // so we need to reset the flag in either case.
- if new&mutexWoken == 0 {
- throw("sync: inconsistent mutex state")
- }
- new &^= mutexWoken
- }
- if atomic.CompareAndSwapInt32(&m.state, old, new) {
- if old&(mutexLocked|mutexStarving) == 0 {
- break // locked the mutex with CAS
- }
- // If we were already waiting before, queue at the front of the queue.
- queueLifo := waitStartTime != 0
- if waitStartTime == 0 {
- waitStartTime = runtime_nanotime()
- }
- runtime_SemacquireMutex(&m.sema, queueLifo, 1)
- starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
- old = m.state
- if old&mutexStarving != 0 {
- // If this goroutine was woken and mutex is in starvation mode,
- // ownership was handed off to us but mutex is in somewhat
- // inconsistent state: mutexLocked is not set and we are still
- // accounted as waiter. Fix that.
- if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
- throw("sync: inconsistent mutex state")
- }
- delta := int32(mutexLocked - 1<<mutexWaiterShift)
- if !starving || old>>mutexWaiterShift == 1 {
- // Exit starvation mode.
- // Critical to do it here and consider wait time.
- // Starvation mode is so inefficient, that two goroutines
- // can go lock-step infinitely once they switch mutex
- // to starvation mode.
- delta -= mutexStarving
- }
- atomic.AddInt32(&m.state, delta)
- break
- }
- awoke = true
- iter = 0
- } else {
- old = m.state
- }
- }
-
- if race.Enabled {
- race.Acquire(unsafe.Pointer(m))
- }
+ return m.mu.TryLock()
}
// Unlock unlocks m.
@@ -214,50 +62,5 @@ func (m *Mutex) lockSlow() {
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
func (m *Mutex) Unlock() {
- if race.Enabled {
- _ = m.state
- race.Release(unsafe.Pointer(m))
- }
-
- // Fast path: drop lock bit.
- 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 {
- fatal("sync: unlock of unlocked mutex")
- }
- if new&mutexStarving == 0 {
- old := new
- for {
- // If there are no waiters or a goroutine has already
- // been woken or grabbed the lock, no need to wake anyone.
- // In starvation mode ownership is directly handed off from unlocking
- // goroutine to the next waiter. We are not part of this chain,
- // since we did not observe mutexStarving when we unlocked the mutex above.
- // So get off the way.
- if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
- return
- }
- // Grab the right to wake someone.
- new = (old - 1<<mutexWaiterShift) | mutexWoken
- if atomic.CompareAndSwapInt32(&m.state, old, new) {
- runtime_Semrelease(&m.sema, false, 1)
- return
- }
- old = m.state
- }
- } else {
- // Starving mode: handoff mutex ownership to the next waiter, and yield
- // our time slice so that the next waiter can start to run immediately.
- // Note: mutexLocked is not set, the waiter will set it after wakeup.
- // But mutex is still considered locked if mutexStarving is set,
- // so new coming goroutines won't acquire it.
- runtime_Semrelease(&m.sema, true, 1)
- }
+ m.mu.Unlock()
}
diff --git a/src/sync/runtime.go b/src/sync/runtime.go
index 5a90813585..b4289dd467 100644
--- a/src/sync/runtime.go
+++ b/src/sync/runtime.go
@@ -21,7 +21,6 @@ func runtime_Semacquire(s *uint32)
// The different forms of this function just tell the runtime how to present
// the reason for waiting in a backtrace, and is used to compute some metrics.
// Otherwise they're functionally identical.
-func runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int)
func runtime_SemacquireRWMutexR(s *uint32, lifo bool, skipframes int)
func runtime_SemacquireRWMutex(s *uint32, lifo bool, skipframes int)
@@ -53,11 +52,5 @@ func init() {
runtime_notifyListCheck(unsafe.Sizeof(n))
}
-// Active spinning runtime support.
-// runtime_canSpin reports whether spinning makes sense at the moment.
-func runtime_canSpin(i int) bool
-
-// runtime_doSpin does active spinning.
-func runtime_doSpin()
-
-func runtime_nanotime() int64
+func throw(string)
+func fatal(string)
diff --git a/src/sync/rwmutex.go b/src/sync/rwmutex.go
index 1d5b8fde4a..7fe096dbd8 100644
--- a/src/sync/rwmutex.go
+++ b/src/sync/rwmutex.go
@@ -64,7 +64,7 @@ const rwmutexMaxReaders = 1 << 30
// documentation on the [RWMutex] type.
func (rw *RWMutex) RLock() {
if race.Enabled {
- _ = rw.w.state
+ race.Read(unsafe.Pointer(&rw.w))
race.Disable()
}
if rw.readerCount.Add(1) < 0 {
@@ -84,7 +84,7 @@ func (rw *RWMutex) RLock() {
// in a particular use of mutexes.
func (rw *RWMutex) TryRLock() bool {
if race.Enabled {
- _ = rw.w.state
+ race.Read(unsafe.Pointer(&rw.w))
race.Disable()
}
for {
@@ -111,7 +111,7 @@ func (rw *RWMutex) TryRLock() bool {
// on entry to RUnlock.
func (rw *RWMutex) RUnlock() {
if race.Enabled {
- _ = rw.w.state
+ race.Read(unsafe.Pointer(&rw.w))
race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
race.Disable()
}
@@ -141,7 +141,7 @@ func (rw *RWMutex) rUnlockSlow(r int32) {
// Lock blocks until the lock is available.
func (rw *RWMutex) Lock() {
if race.Enabled {
- _ = rw.w.state
+ race.Read(unsafe.Pointer(&rw.w))
race.Disable()
}
// First, resolve competition with other writers.
@@ -166,7 +166,7 @@ func (rw *RWMutex) Lock() {
// in a particular use of mutexes.
func (rw *RWMutex) TryLock() bool {
if race.Enabled {
- _ = rw.w.state
+ race.Read(unsafe.Pointer(&rw.w))
race.Disable()
}
if !rw.w.TryLock() {
@@ -198,7 +198,7 @@ func (rw *RWMutex) TryLock() bool {
// arrange for another goroutine to [RWMutex.RUnlock] ([RWMutex.Unlock]) it.
func (rw *RWMutex) Unlock() {
if race.Enabled {
- _ = rw.w.state
+ race.Read(unsafe.Pointer(&rw.w))
race.Release(unsafe.Pointer(&rw.readerSem))
race.Disable()
}