diff --git a/Makefile b/Makefile index 569defc0..06dce5a3 100644 --- a/Makefile +++ b/Makefile @@ -234,6 +234,7 @@ TEST_PACKAGES = \ os \ path \ reflect \ + sync \ testing \ testing/iotest \ text/scanner \ diff --git a/src/sync/cond_test.go b/src/sync/cond_test.go new file mode 100644 index 00000000..6303ab96 --- /dev/null +++ b/src/sync/cond_test.go @@ -0,0 +1,83 @@ +package sync_test + +import ( + "sync" + "sync/atomic" + "testing" +) + +// TestCondSignal tests waiting on a Cond and notifying it with Signal. +func TestCondSignal(t *testing.T) { + // Create a Cond with a normal mutex. + cond := sync.Cond{ + L: &sync.Mutex{}, + } + cond.L.Lock() + + // Start a goroutine to signal us once we wait. + var signaled uint32 + go func() { + // Wait for the test goroutine to wait. + cond.L.Lock() + defer cond.L.Unlock() + + // Send a signal to the test goroutine. + atomic.StoreUint32(&signaled, 1) + cond.Signal() + }() + + // Wait for a signal. + // This will unlock the mutex, and allow the spawned goroutine to run. + cond.Wait() + if atomic.LoadUint32(&signaled) == 0 { + t.Error("wait returned before a signal was sent") + } +} + +func TestCondBroadcast(t *testing.T) { + // Create a Cond with an RWMutex. + var mu sync.RWMutex + cond := sync.Cond{ + L: mu.RLocker(), + } + + // Start goroutines to wait for the broadcast. + var wg sync.WaitGroup + const n = 5 + for i := 0; i < n; i++ { + wg.Add(1) + mu.RLock() + go func() { + defer wg.Done() + + cond.Wait() + }() + } + + // Wait for all goroutines to start waiting. + mu.Lock() + + // Broadcast to all of the waiting goroutines. + cond.Broadcast() + + // Wait for all spawned goroutines to process the broadcast. + mu.Unlock() + wg.Wait() +} + +// TestCondUnlockNotify verifies that a signal is processed even if it happens during the mutex unlock in Wait. +func TestCondUnlockNotify(t *testing.T) { + // Create a Cond that signals itself when waiting. + var cond sync.Cond + cond.L = fakeLocker{cond.Signal} + + cond.Wait() +} + +// fakeLocker is a fake sync.Locker where unlock calls an arbitrary function. +type fakeLocker struct { + unlock func() +} + +func (l fakeLocker) Lock() {} +func (l fakeLocker) Unlock() { l.unlock() } diff --git a/src/sync/mutex_test.go b/src/sync/mutex_test.go new file mode 100644 index 00000000..88ae317d --- /dev/null +++ b/src/sync/mutex_test.go @@ -0,0 +1,205 @@ +package sync_test + +import ( + "runtime" + "sync" + "sync/atomic" + "testing" +) + +// TestMutexUncontended tests locking and unlocking a Mutex that is not shared with any other goroutines. +func TestMutexUncontended(t *testing.T) { + var mu sync.Mutex + + // Lock and unlock the mutex a few times. + for i := 0; i < 3; i++ { + mu.Lock() + mu.Unlock() + } +} + +// TestMutexConcurrent tests a mutex concurrently from multiple goroutines. +// It will fail if multiple goroutines hold the lock simultaneously. +func TestMutexConcurrent(t *testing.T) { + var mu sync.Mutex + var active uint + var completed uint + ok := true + + const n = 10 + for i := 0; i < n; i++ { + j := i + go func() { + // Delay a bit. + for k := j; k > 0; k-- { + runtime.Gosched() + } + + mu.Lock() + + // Increment the active counter. + active++ + + if active > 1 { + // Multiple things are holding the lock at the same time. + ok = false + } else { + // Delay a bit. + for k := j; k < n; k++ { + runtime.Gosched() + } + } + + // Decrement the active counter. + active-- + + // This is completed. + completed++ + + mu.Unlock() + }() + } + + // Wait for everything to finish. + var done bool + for !done { + // Wait a bit for other things to run. + runtime.Gosched() + + // Acquire the lock and check whether everything has completed. + mu.Lock() + done = completed == n + mu.Unlock() + } + if !ok { + t.Error("lock held concurrently") + } +} + +// TestRWMutexUncontended tests locking and unlocking an RWMutex that is not shared with any other goroutines. +func TestRWMutexUncontended(t *testing.T) { + var mu sync.RWMutex + + // Lock the mutex exclusively and then unlock it. + mu.Lock() + mu.Unlock() + + // Acuire several read locks. + const n = 5 + for i := 0; i < n; i++ { + mu.RLock() + } + + // Release all of the read locks. + for i := 0; i < n; i++ { + mu.RUnlock() + } + + // Re-acquire the lock exclusively. + mu.Lock() + mu.Unlock() +} + +// TestRWMutexWriteToRead tests the transition from a write lock to a read lock while contended. +func TestRWMutexWriteToRead(t *testing.T) { + // Create a new RWMutex and acquire a write lock. + var mu sync.RWMutex + mu.Lock() + + const n = 3 + var readAcquires uint32 + var completed uint32 + var unlocked uint32 + var bad uint32 + for i := 0; i < n; i++ { + go func() { + // Acquire a read lock. + mu.RLock() + + // Verify that the write lock is supposed to be released by now. + if atomic.LoadUint32(&unlocked) == 0 { + // The write lock is still being held. + atomic.AddUint32(&bad, 1) + } + + // Add ourselves to the read lock counter. + atomic.AddUint32(&readAcquires, 1) + + // Wait for everything to hold the read lock simultaneously. + for atomic.LoadUint32(&readAcquires) < n { + runtime.Gosched() + } + + // Notify of completion. + atomic.AddUint32(&completed, 1) + + // Release the read lock. + mu.RUnlock() + }() + } + + // Wait a bit for the goroutines to block. + for i := 0; i < 3*n; i++ { + runtime.Gosched() + } + + // Release the write lock so that the goroutines acquire read locks. + atomic.StoreUint32(&unlocked, 1) + mu.Unlock() + + // Wait for everything to complete. + for atomic.LoadUint32(&completed) < n { + runtime.Gosched() + } + + // Acquire another write lock. + mu.Lock() + + if bad != 0 { + t.Error("read lock acquired while write-locked") + } +} + +// TestRWMutexWriteToRead tests the transition from a read lock to a write lock while contended. +func TestRWMutexReadToWrite(t *testing.T) { + // Create a new RWMutex and read-lock it several times. + const n = 3 + var mu sync.RWMutex + var readers uint32 + for i := 0; i < n; i++ { + mu.RLock() + readers++ + } + + // Start a goroutine to acquire a write lock. + result := ^uint32(0) + go func() { + // Acquire a write lock. + mu.Lock() + + // Check for active readers. + readers := atomic.LoadUint32(&readers) + + mu.Unlock() + + // Report the number of active readers. + atomic.StoreUint32(&result, readers) + }() + + // Release the read locks. + for i := 0; i < n; i++ { + runtime.Gosched() + atomic.AddUint32(&readers, ^uint32(0)) + mu.RUnlock() + } + + // Wait for a result. + var res uint32 + for res == ^uint32(0) { + runtime.Gosched() + res = atomic.LoadUint32(&result) + } + if res != 0 { + t.Errorf("write lock acquired while %d readers were active", res) + } +} diff --git a/src/sync/once_test.go b/src/sync/once_test.go new file mode 100644 index 00000000..f6d12d70 --- /dev/null +++ b/src/sync/once_test.go @@ -0,0 +1,62 @@ +package sync_test + +import ( + "sync" + "testing" +) + +// TestOnceUncontended tests Once on a single goroutine. +func TestOnceUncontended(t *testing.T) { + var once sync.Once + { + var ran bool + once.Do(func() { + ran = true + }) + if !ran { + t.Error("first call to Do did not run") + } + } + { + var ran bool + once.Do(func() { + ran = true + }) + if ran { + t.Error("second call to Do ran") + } + } +} + +// TestOnceConcurrent tests multiple concurrent invocations of sync.Once. +func TestOnceConcurrent(t *testing.T) { + var once sync.Once + var mu sync.Mutex + mu.Lock() + var ran bool + var ranTwice bool + once.Do(func() { + ran = true + + // Start a goroutine and (approximately) wait for it to enter the call to Do. + var startWait sync.Mutex + startWait.Lock() + go func() { + startWait.Unlock() + once.Do(func() { + ranTwice = true + }) + mu.Unlock() + }() + startWait.Lock() + }) + if !ran { + t.Error("first call to Do did not run") + } + + // Wait for the goroutine to finish. + mu.Lock() + if ranTwice { + t.Error("second concurrent call to Once also ran") + } +} diff --git a/src/sync/waitgroup_test.go b/src/sync/waitgroup_test.go new file mode 100644 index 00000000..a808f7e1 --- /dev/null +++ b/src/sync/waitgroup_test.go @@ -0,0 +1,35 @@ +package sync_test + +import ( + "sync" + "testing" +) + +// TestWaitGroupUncontended tests the wait group from a single goroutine. +func TestWaitGroupUncontended(t *testing.T) { + // Check that a single add-and-done works. + var wg sync.WaitGroup + wg.Add(1) + wg.Done() + wg.Wait() + + // Check that mixing positive and negative counts works. + wg.Add(10) + wg.Add(-8) + wg.Add(-1) + wg.Add(0) + wg.Done() + wg.Wait() +} + +// TestWaitGroup tests the typical usage of WaitGroup. +func TestWaitGroup(t *testing.T) { + const n = 5 + var wg sync.WaitGroup + wg.Add(n) + for i := 0; i < n; i++ { + go wg.Done() + } + + wg.Wait() +}