syncs: add MutexValue (#14422)

MutexValue is simply a value guarded by a mutex.
For any type that is not pointer-sized,
MutexValue will perform much better than AtomicValue
since it will not incur an allocation boxing the value
into an interface value (which is how Go's atomic.Value
is implemented under-the-hood).

Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This commit is contained in:
Joe Tsai 2024-12-18 17:11:22 -08:00 committed by GitHub
parent b3d4ffe168
commit ff5b4bae99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 96 additions and 0 deletions

View File

@ -25,6 +25,7 @@ func initClosedChan() <-chan struct{} {
}
// AtomicValue is the generic version of [atomic.Value].
// See [MutexValue] for guidance on whether to use this type.
type AtomicValue[T any] struct {
v atomic.Value
}
@ -74,6 +75,67 @@ func (v *AtomicValue[T]) CompareAndSwap(oldV, newV T) (swapped bool) {
return v.v.CompareAndSwap(wrappedValue[T]{oldV}, wrappedValue[T]{newV})
}
// MutexValue is a value protected by a mutex.
//
// AtomicValue, [MutexValue], [atomic.Pointer] are similar and
// overlap in their use cases.
//
// - Use [atomic.Pointer] if the value being stored is a pointer and
// you only ever need load and store operations.
// An atomic pointer only occupies 1 word of memory.
//
// - Use [MutexValue] if the value being stored is not a pointer or
// you need the ability for a mutex to protect a set of operations
// performed on the value.
// A mutex-guarded value occupies 1 word of memory plus
// the memory representation of T.
//
// - AtomicValue is useful for non-pointer types that happen to
// have the memory layout of a single pointer.
// Examples include a map, channel, func, or a single field struct
// that contains any prior types.
// An atomic value occupies 2 words of memory.
// Consequently, Storing of non-pointer types always allocates.
//
// Note that [AtomicValue] has the ability to report whether it was set
// while [MutexValue] lacks the ability to detect if the value was set
// and it happens to be the zero value of T. If such a use case is
// necessary, then you could consider wrapping T in [opt.Value].
type MutexValue[T any] struct {
mu sync.Mutex
v T
}
// WithLock calls f with a pointer to the value while holding the lock.
// The provided pointer must not leak beyond the scope of the call.
func (m *MutexValue[T]) WithLock(f func(p *T)) {
m.mu.Lock()
defer m.mu.Unlock()
f(&m.v)
}
// Load returns a shallow copy of the underlying value.
func (m *MutexValue[T]) Load() T {
m.mu.Lock()
defer m.mu.Unlock()
return m.v
}
// Store stores a shallow copy of the provided value.
func (m *MutexValue[T]) Store(v T) {
m.mu.Lock()
defer m.mu.Unlock()
m.v = v
}
// Swap stores new into m and returns the previous value.
func (m *MutexValue[T]) Swap(new T) (old T) {
m.mu.Lock()
defer m.mu.Unlock()
old, m.v = m.v, new
return old
}
// WaitGroupChan is like a sync.WaitGroup, but has a chan that closes
// on completion that you can wait on. (This, you can only use the
// value once)

View File

@ -8,6 +8,7 @@ import (
"io"
"os"
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
@ -65,6 +66,39 @@ func TestAtomicValue(t *testing.T) {
}
}
func TestMutexValue(t *testing.T) {
var v MutexValue[time.Time]
if n := int(testing.AllocsPerRun(1000, func() {
v.Store(v.Load())
v.WithLock(func(*time.Time) {})
})); n != 0 {
t.Errorf("AllocsPerRun = %d, want 0", n)
}
now := time.Now()
v.Store(now)
if !v.Load().Equal(now) {
t.Errorf("Load = %v, want %v", v.Load(), now)
}
var group WaitGroup
var v2 MutexValue[int]
var sum int
for i := range 10 {
group.Go(func() {
old1 := v2.Load()
old2 := v2.Swap(old1 + i)
delta := old2 - old1
v2.WithLock(func(p *int) { *p += delta })
})
sum += i
}
group.Wait()
if v2.Load() != sum {
t.Errorf("Load = %v, want %v", v2.Load(), sum)
}
}
func TestWaitGroupChan(t *testing.T) {
wg := NewWaitGroupChan()