Browse Source

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 <[email protected]>
Joe Tsai 1 year ago
parent
commit
ff5b4bae99
2 changed files with 96 additions and 0 deletions
  1. 62 0
      syncs/syncs.go
  2. 34 0
      syncs/syncs_test.go

+ 62 - 0
syncs/syncs.go

@@ -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)

+ 34 - 0
syncs/syncs_test.go

@@ -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()