fix syncx dead lock

This commit is contained in:
georgehao 2025-02-04 15:20:28 +08:00
parent e26dd774a9
commit 288d85f411
No known key found for this signature in database
2 changed files with 127 additions and 20 deletions

View file

@ -17,36 +17,62 @@
// Package syncx contains exotic synchronization primitives. // Package syncx contains exotic synchronization primitives.
package syncx package syncx
// ClosableMutex is a mutex that can also be closed. import (
// Once closed, it can never be taken again. "sync"
)
// ClosableMutex is a mutex that can be closed. Once closed, it cannot be locked again.
type ClosableMutex struct { type ClosableMutex struct {
mu sync.Mutex // Protects the following fields
closed bool
ch chan struct{} ch chan struct{}
} }
// NewClosableMutex creates a new closable mutex.
func NewClosableMutex() *ClosableMutex { func NewClosableMutex() *ClosableMutex {
ch := make(chan struct{}, 1) ch := make(chan struct{}, 1)
ch <- struct{}{} ch <- struct{}{}
return &ClosableMutex{ch} return &ClosableMutex{ch: ch}
} }
// TryLock attempts to lock cm. // TryLock attempts to acquire the lock. Returns true if successful, false if the lock is closed or unavailable.
// If the mutex is closed, TryLock returns false.
func (cm *ClosableMutex) TryLock() bool { func (cm *ClosableMutex) TryLock() bool {
_, ok := <-cm.ch cm.mu.Lock()
return ok defer cm.mu.Unlock()
if cm.closed {
return false
} }
// MustLock locks cm. select {
// If the mutex is closed, MustLock panics. case <-cm.ch:
return true
default:
return false
}
}
// MustLock acquires the lock. Panics if the lock is already closed.
func (cm *ClosableMutex) MustLock() { func (cm *ClosableMutex) MustLock() {
_, ok := <-cm.ch cm.mu.Lock()
if !ok { defer cm.mu.Unlock()
if cm.closed {
panic("mutex closed") panic("mutex closed")
} }
select {
case <-cm.ch:
return
default:
panic("mutex is already locked")
}
} }
// Unlock unlocks cm. // Unlock releases the lock. Panics if the lock is already closed or if called without holding the lock.
func (cm *ClosableMutex) Unlock() { func (cm *ClosableMutex) Unlock() {
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.closed {
panic("Unlock after Close")
}
select { select {
case cm.ch <- struct{}{}: case cm.ch <- struct{}{}:
default: default:
@ -54,11 +80,13 @@ func (cm *ClosableMutex) Unlock() {
} }
} }
// Close locks the mutex, then closes it. // Close closes the mutex, preventing further lock operations. Panics if called on an already-closed mutex.
func (cm *ClosableMutex) Close() { func (cm *ClosableMutex) Close() {
_, ok := <-cm.ch cm.mu.Lock()
if !ok { defer cm.mu.Unlock()
if cm.closed {
panic("Close of already-closed ClosableMutex") panic("Close of already-closed ClosableMutex")
} }
close(cm.ch) cm.closed = true
close(cm.ch) // Closing the channel will cause subsequent send operations to panic
} }

View file

@ -0,0 +1,79 @@
package syncx
import (
"testing"
"time"
)
func TestClosableMutex_TryLock(t *testing.T) {
cm := NewClosableMutex()
if !cm.TryLock() {
t.Fatal("expected TryLock to succeed")
}
if cm.TryLock() {
t.Fatal("expected TryLock to fail when already locked")
}
cm.Unlock()
if !cm.TryLock() {
t.Fatal("expected TryLock to succeed after unlock")
}
cm.Close()
if cm.TryLock() {
t.Fatal("expected TryLock to fail after close")
}
}
func TestClosableMutex_MustLock(t *testing.T) {
cm := NewClosableMutex()
cm.MustLock()
defer func() {
if r := recover(); r == nil {
t.Fatal("expected MustLock to panic when already locked")
}
}()
cm.MustLock()
}
func TestClosableMutex_Unlock(t *testing.T) {
cm := NewClosableMutex()
cm.MustLock()
cm.Unlock()
defer func() {
if r := recover(); r == nil {
t.Fatal("expected Unlock to panic when already unlocked")
}
}()
cm.Unlock()
}
func TestClosableMutex_Close(t *testing.T) {
cm := NewClosableMutex()
cm.MustLock()
cm.Close()
defer func() {
if r := recover(); r == nil {
t.Fatal("expected Close to panic when already closed")
}
}()
cm.Close()
}
func TestClosableMutex_Concurrent(t *testing.T) {
cm := NewClosableMutex()
done := make(chan struct{})
go func() {
cm.MustLock()
time.Sleep(100 * time.Millisecond)
cm.Unlock()
close(done)
}()
time.Sleep(50 * time.Millisecond)
if cm.TryLock() {
t.Fatal("expected TryLock to fail when locked by another goroutine")
}
<-done
if !cm.TryLock() {
t.Fatal("expected TryLock to succeed after other goroutine unlocks")
}
cm.Unlock()
}