mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 02:24:25 -05:00
Bump github.com/sasha-s/go-deadlock from 0.3.6 to 0.3.9
Bumps [github.com/sasha-s/go-deadlock](https://github.com/sasha-s/go-deadlock) from 0.3.6 to 0.3.9. - [Release notes](https://github.com/sasha-s/go-deadlock/releases) - [Commits](https://github.com/sasha-s/go-deadlock/compare/v0.3.6...v0.3.9) --- updated-dependencies: - dependency-name: github.com/sasha-s/go-deadlock dependency-version: 0.3.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
@@ -30,7 +30,7 @@ require (
|
||||
github.com/sahilm/fuzzy v0.1.1
|
||||
github.com/samber/lo v1.31.0
|
||||
github.com/sanity-io/litter v1.5.8
|
||||
github.com/sasha-s/go-deadlock v0.3.6
|
||||
github.com/sasha-s/go-deadlock v0.3.9
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spkg/bom v1.0.1
|
||||
|
||||
@@ -109,8 +109,8 @@ github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
|
||||
github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
|
||||
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
|
||||
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||
github.com/sasha-s/go-deadlock v0.3.6 h1:TR7sfOnZ7x00tWPfD397Peodt57KzMDo+9Ae9rMiUmw=
|
||||
github.com/sasha-s/go-deadlock v0.3.6/go.mod h1:CUqNyyvMxTyjFqDT7MRg9mb4Dv/btmGTqSR+rky/UXo=
|
||||
github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w=
|
||||
github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
|
||||
+32
-3
@@ -1,4 +1,4 @@
|
||||
# Online deadlock detection in go (golang). [](https://wandbox.org/permlink/hJc6QCZowxbNm9WW) [](https://godoc.org/github.com/sasha-s/go-deadlock) [](https://codecov.io/gh/sasha-s/go-deadlock) [](https://github.com/sasha-s/go-deadlock/releases) [](https://goreportcard.com/report/github.com/sasha-s/go-deadlock) [](https://opensource.org/licenses/Apache-2.0)
|
||||
# Online deadlock detection in go (golang). [](https://wandbox.org/permlink/hJc6QCZowxbNm9WW) [](https://godoc.org/github.com/sasha-s/go-deadlock) [](https://codecov.io/gh/sasha-s/go-deadlock) [](https://github.com/sasha-s/go-deadlock/releases) [](https://goreportcard.com/report/github.com/sasha-s/go-deadlock) [](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
## Why
|
||||
Deadlocks happen and are painful to debug.
|
||||
@@ -172,16 +172,45 @@ func main() {
|
||||
rlockTwice()
|
||||
}
|
||||
```
|
||||
## Build Tags and Compatibility Modes
|
||||
|
||||
go-deadlock supports multiple build configurations for different use cases:
|
||||
|
||||
* **Normal mode** (default): Full deadlock detection with timer pooling
|
||||
* **Synctest mode**: Compatible with Go's `testing/synctest` package - use either:
|
||||
* `-tags=deadlock_synctest` (recommended for Go 1.25+)
|
||||
* `-tags=goexperiment.synctest` (for experimental synctest)
|
||||
* **Disabled mode** (`-tags=deadlock_disable`): Zero overhead, no detection
|
||||
|
||||
**Why synctest mode?** `sync.Mutex` is not durably blocking in synctest bubbles, but channels are. Synctest mode uses channel-based mutexes to ensure proper behavior with `testing/synctest`.
|
||||
|
||||
### Quick Examples
|
||||
|
||||
```bash
|
||||
# Normal development/testing
|
||||
go test ./...
|
||||
|
||||
# Testing with synctest (Go 1.25+, recommended)
|
||||
GODEBUG=asynctimerchan=0 go test -tags=deadlock_synctest ./...
|
||||
|
||||
# Testing with experimental synctest
|
||||
GODEBUG=asynctimerchan=0 go test -tags=goexperiment.synctest ./...
|
||||
|
||||
# Production build with zero overhead
|
||||
go build -tags=deadlock_disable ./...
|
||||
```
|
||||
|
||||
## Configuring go-deadlock
|
||||
|
||||
Have a look at [Opts](https://pkg.go.dev/github.com/sasha-s/go-deadlock#pkg-variables).
|
||||
|
||||
* `Opts.Disable`: disables deadlock detection altogether
|
||||
* `Opts.Disable`: disables deadlock detection altogether (runtime option; see also `deadlock_disable` build tag)
|
||||
* `Opts.DisableLockOrderDetection`: disables lock order based deadlock detection.
|
||||
* `Opts.DeadlockTimeout`: blocking on mutex for longer than DeadlockTimeout is considered a deadlock. ignored if negative
|
||||
* `Opts.OnPotentialDeadlock`: callback for then deadlock is detected
|
||||
* `Opts.MaxMapSize`: size of happens before // happens after table
|
||||
* `Opts.PrintAllCurrentGoroutines`: dump stacktraces of all goroutines when inconsistent locking is detected, verbose
|
||||
* `Opts.LogBuf`: where to write deadlock info/stacktraces
|
||||
* `Opts.TimerPool`: controls timer pooling behavior (auto-configured based on build tags)
|
||||
|
||||
|
||||
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
//go:build go1.24
|
||||
|
||||
package deadlock
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
"weak"
|
||||
)
|
||||
|
||||
type beforeAfter struct {
|
||||
before weak.Pointer[byte]
|
||||
after weak.Pointer[byte]
|
||||
}
|
||||
|
||||
// ptrFromInterface extracts the data pointer from an interface{} value.
|
||||
// An interface (eface) is {type *_type, data unsafe.Pointer}; we grab the second word.
|
||||
func ptrFromInterface(i interface{}) *byte {
|
||||
type eface struct {
|
||||
_ uintptr
|
||||
data unsafe.Pointer
|
||||
}
|
||||
return (*byte)((*eface)(unsafe.Pointer(&i)).data)
|
||||
}
|
||||
|
||||
func newBeforeAfter(before, after interface{}) beforeAfter {
|
||||
return beforeAfter{
|
||||
before: weak.Make(ptrFromInterface(before)),
|
||||
after: weak.Make(ptrFromInterface(after)),
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
//go:build !go1.24
|
||||
|
||||
package deadlock
|
||||
|
||||
type beforeAfter struct {
|
||||
before interface{}
|
||||
after interface{}
|
||||
}
|
||||
|
||||
func newBeforeAfter(before, after interface{}) beforeAfter {
|
||||
return beforeAfter{before: before, after: after}
|
||||
}
|
||||
+284
-127
@@ -7,11 +7,24 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/petermattis/goid"
|
||||
)
|
||||
|
||||
// TimerPoolMode controls timer pooling behavior
|
||||
type TimerPoolMode int
|
||||
|
||||
const (
|
||||
// TimerPoolDefault automatically chooses based on build environment
|
||||
TimerPoolDefault TimerPoolMode = iota
|
||||
// TimerPoolEnabled always uses timer pooling for performance
|
||||
TimerPoolEnabled
|
||||
// TimerPoolDisabled disables timer pooling (required for testing/synctest)
|
||||
TimerPoolDisabled
|
||||
)
|
||||
|
||||
// Opts control how deadlock detection behaves.
|
||||
// Options are supposed to be set once at a startup (say, when parsing flags).
|
||||
var Opts = struct {
|
||||
@@ -31,7 +44,12 @@ var Opts = struct {
|
||||
MaxMapSize int
|
||||
// Will dump stacktraces of all goroutines when inconsistent locking is detected.
|
||||
PrintAllCurrentGoroutines bool
|
||||
mu *sync.Mutex // Protects the LogBuf.
|
||||
// Controls timer pooling behavior.
|
||||
// TimerPoolDefault: Automatically choose based on build environment
|
||||
// TimerPoolEnabled: Always use timer pooling
|
||||
// TimerPoolDisabled: Never use timer pooling
|
||||
TimerPool TimerPoolMode
|
||||
mu *sync.Mutex // Protects the LogBuf.
|
||||
// Will print deadlock info to log buffer.
|
||||
LogBuf io.Writer
|
||||
}{
|
||||
@@ -75,7 +93,7 @@ var NewCond = sync.NewCond
|
||||
// A Mutex is a drop-in replacement for sync.Mutex.
|
||||
// Performs deadlock detection unless disabled in Opts.
|
||||
type Mutex struct {
|
||||
mu sync.Mutex
|
||||
mu StandardMutex
|
||||
}
|
||||
|
||||
// Lock locks the mutex.
|
||||
@@ -104,7 +122,7 @@ func (m *Mutex) Unlock() {
|
||||
// An RWMutex is a drop-in replacement for sync.RWMutex.
|
||||
// Performs deadlock detection unless disabled in Opts.
|
||||
type RWMutex struct {
|
||||
mu sync.RWMutex
|
||||
mu StandardRWMutex
|
||||
}
|
||||
|
||||
// Lock locks rw for writing.
|
||||
@@ -155,15 +173,15 @@ func (m *RWMutex) RUnlock() {
|
||||
// RLocker returns a Locker interface that implements
|
||||
// the Lock and Unlock methods by calling RLock and RUnlock.
|
||||
func (m *RWMutex) RLocker() sync.Locker {
|
||||
return (*rlocker)(m)
|
||||
return m.mu.RLocker()
|
||||
}
|
||||
|
||||
func preLock(stack []uintptr, p interface{}) {
|
||||
lo.preLock(stack, p)
|
||||
}
|
||||
|
||||
func postLock(stack []uintptr, p interface{}) {
|
||||
lo.postLock(stack, p)
|
||||
func postLock(stack []uintptr, buf *[stackBufSize]uintptr, p interface{}) {
|
||||
lo.postLock(stack, buf, p)
|
||||
}
|
||||
|
||||
func postUnlock(p interface{}) {
|
||||
@@ -175,104 +193,180 @@ func lock(lockFn func(), ptr interface{}) {
|
||||
lockFn()
|
||||
return
|
||||
}
|
||||
stack := callers(1)
|
||||
stack, buf := callers(1)
|
||||
// Cache timeout before preLock so all Opts reads complete before preLock
|
||||
// may call OnPotentialDeadlock. If preLock detects a problem (recursive
|
||||
// lock, order violation) the goroutine may block forever in lockFn below,
|
||||
// and reading Opts after preLock would race with any later Opts write.
|
||||
timeout := Opts.DeadlockTimeout
|
||||
preLock(stack, ptr)
|
||||
if Opts.DeadlockTimeout <= 0 {
|
||||
if timeout <= 0 {
|
||||
lockFn()
|
||||
} else {
|
||||
ch := make(chan struct{})
|
||||
currentID := goid.Get()
|
||||
go checkDeadlock(stack, ptr, currentID, ch)
|
||||
e := dw.register(stack, ptr, currentID, timeout)
|
||||
lockFn()
|
||||
postLock(stack, ptr)
|
||||
close(ch)
|
||||
dw.deregister(e)
|
||||
postLock(stack, buf, ptr)
|
||||
return
|
||||
}
|
||||
postLock(stack, ptr)
|
||||
postLock(stack, buf, ptr)
|
||||
}
|
||||
|
||||
var timersPool sync.Pool
|
||||
|
||||
func acquireTimer(d time.Duration) *time.Timer {
|
||||
t, ok := timersPool.Get().(*time.Timer)
|
||||
if ok {
|
||||
_ = t.Reset(d)
|
||||
return t
|
||||
}
|
||||
return time.NewTimer(Opts.DeadlockTimeout)
|
||||
// pendingEntry tracks a goroutine that is waiting to acquire a lock. Entries are
|
||||
// pooled to avoid per-lock heap allocations (goroutine stacks, channels, closures).
|
||||
//
|
||||
// Timer safety invariants:
|
||||
// - checkFn is allocated once per entry and reused across pool cycles, so recycling
|
||||
// an entry does not allocate a new closure.
|
||||
// - The done flag synchronizes the callback with deregister: deregister sets done=1
|
||||
// before calling Stop(), and the callback checks done before acting. Because both
|
||||
// use atomic operations, the callback is guaranteed to observe done=1 if deregister
|
||||
// has already run, even if the runtime already scheduled the callback.
|
||||
// - An entry is only returned to the pool when timer.Stop() returns true, meaning
|
||||
// the timer was successfully cancelled and the callback will never run. This prevents
|
||||
// a recycled entry from being mutated by an in-flight callback.
|
||||
// - When Stop() returns false (callback already firing or queued), the entry is
|
||||
// intentionally leaked to GC. This only happens in the rare deadlock-timeout path.
|
||||
type pendingEntry struct {
|
||||
stack []uintptr
|
||||
ptr interface{}
|
||||
gid int64
|
||||
done int32 // atomic: 0=pending, 1=acquired
|
||||
timer *time.Timer
|
||||
checkFn func()
|
||||
}
|
||||
|
||||
func releaseTimer(t *time.Timer) {
|
||||
if !t.Stop() {
|
||||
<-t.C
|
||||
}
|
||||
timersPool.Put(t)
|
||||
}
|
||||
|
||||
func checkDeadlock(stack []uintptr, ptr interface{}, currentID int64, ch <-chan struct{}) {
|
||||
t := acquireTimer(Opts.DeadlockTimeout)
|
||||
defer releaseTimer(t)
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
lo.mu.Lock()
|
||||
prev, ok := lo.cur[ptr]
|
||||
if !ok {
|
||||
lo.mu.Unlock()
|
||||
break // Nobody seems to be holding the lock, try again.
|
||||
}
|
||||
Opts.mu.Lock()
|
||||
fmt.Fprintln(Opts.LogBuf, header)
|
||||
fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed")
|
||||
fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", prev.gid, ptr)
|
||||
printStack(Opts.LogBuf, prev.stack)
|
||||
fmt.Fprintln(Opts.LogBuf, "Have been trying to lock it again for more than", Opts.DeadlockTimeout)
|
||||
fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", currentID, ptr)
|
||||
printStack(Opts.LogBuf, stack)
|
||||
stacks := stacks()
|
||||
grs := bytes.Split(stacks, []byte("\n\n"))
|
||||
for _, g := range grs {
|
||||
if goid.ExtractGID(g) == prev.gid {
|
||||
fmt.Fprintln(Opts.LogBuf, "Here is what goroutine", prev.gid, "doing now")
|
||||
Opts.LogBuf.Write(g)
|
||||
fmt.Fprintln(Opts.LogBuf)
|
||||
}
|
||||
}
|
||||
lo.other(ptr)
|
||||
if Opts.PrintAllCurrentGoroutines {
|
||||
fmt.Fprintln(Opts.LogBuf, "All current goroutines:")
|
||||
Opts.LogBuf.Write(stacks)
|
||||
}
|
||||
fmt.Fprintln(Opts.LogBuf)
|
||||
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
|
||||
buf.Flush()
|
||||
}
|
||||
Opts.mu.Unlock()
|
||||
lo.mu.Unlock()
|
||||
Opts.OnPotentialDeadlock()
|
||||
<-ch
|
||||
return
|
||||
case <-ch:
|
||||
func newPendingEntry() *pendingEntry {
|
||||
e := &pendingEntry{}
|
||||
// Capture e by pointer so the closure is stable across pool reuse, no new
|
||||
// closure allocation when the entry is recycled.
|
||||
e.checkFn = func() {
|
||||
// If the lock was acquired (done=1), the entry may already be back in the
|
||||
// pool or being reused. Bail out unconditionally.
|
||||
if atomic.LoadInt32(&e.done) != 0 {
|
||||
return
|
||||
}
|
||||
t.Reset(Opts.DeadlockTimeout)
|
||||
onDeadlockTimeout(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
var pendingPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return newPendingEntry()
|
||||
},
|
||||
}
|
||||
|
||||
type deadlockWatcher struct{}
|
||||
|
||||
var dw deadlockWatcher
|
||||
|
||||
func (w *deadlockWatcher) register(stack []uintptr, ptr interface{}, gid int64, timeout time.Duration) *pendingEntry {
|
||||
var e *pendingEntry
|
||||
if shouldDisableTimerPool() {
|
||||
e = newPendingEntry()
|
||||
} else {
|
||||
e = pendingPool.Get().(*pendingEntry)
|
||||
}
|
||||
e.stack = stack
|
||||
e.ptr = ptr
|
||||
e.gid = gid
|
||||
atomic.StoreInt32(&e.done, 0)
|
||||
if e.timer == nil {
|
||||
// First use (freshly allocated entry): create the AfterFunc timer.
|
||||
// AfterFunc avoids the channel-drain problems of channel-based timers,
|
||||
// which are especially problematic under testing/synctest.
|
||||
e.timer = time.AfterFunc(timeout, e.checkFn)
|
||||
} else {
|
||||
// Reused from pool: the timer was previously Stop()'d successfully
|
||||
// (guaranteed by deregister), so Reset is safe here.
|
||||
e.timer.Reset(timeout)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// deregister marks the lock as acquired and cancels the deadlock timer.
|
||||
// Must be called exactly once per register call. The entry pointer is
|
||||
// stack-local in lock(), so concurrent or duplicate calls cannot occur.
|
||||
func (w *deadlockWatcher) deregister(e *pendingEntry) {
|
||||
// Mark done BEFORE stopping the timer. The callback checks done with an
|
||||
// atomic load, so even if the timer fires concurrently, the callback will
|
||||
// see done=1 and return without acting.
|
||||
atomic.StoreInt32(&e.done, 1)
|
||||
stopped := e.timer.Stop()
|
||||
// Only recycle the entry if Stop() confirmed the callback won't run.
|
||||
// If Stop() returned false the callback is already executing or queued;
|
||||
// recycling would race with the callback reading entry fields.
|
||||
if stopped && !shouldDisableTimerPool() {
|
||||
e.stack = nil
|
||||
e.ptr = nil
|
||||
e.gid = 0
|
||||
pendingPool.Put(e)
|
||||
}
|
||||
}
|
||||
|
||||
func onDeadlockTimeout(e *pendingEntry) {
|
||||
lo.mu.Lock()
|
||||
holders, ok := lo.cur[e.ptr]
|
||||
if !ok || len(holders) == 0 {
|
||||
// Lock appears unheld (transient state, holder may have just released).
|
||||
// Reschedule if the waiter is still pending. Note: this creates a new timer
|
||||
// (e.timer is not updated), so if deregister runs later it will Stop() the
|
||||
// original (already-fired) timer, get false, and skip pooling. The new timer's
|
||||
// callback will then observe done=1 and no-op. This is safe but means the
|
||||
// entry won't be recycled, acceptable since this is the rare timeout path.
|
||||
lo.mu.Unlock()
|
||||
if atomic.LoadInt32(&e.done) == 0 {
|
||||
time.AfterFunc(Opts.DeadlockTimeout, e.checkFn)
|
||||
}
|
||||
return
|
||||
}
|
||||
Opts.mu.Lock()
|
||||
fmt.Fprintln(Opts.LogBuf, header)
|
||||
for _, prev := range holders {
|
||||
fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed")
|
||||
fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", prev.gid, e.ptr)
|
||||
printStack(Opts.LogBuf, prev.stack)
|
||||
}
|
||||
fmt.Fprintln(Opts.LogBuf, "Have been trying to lock it again for more than", Opts.DeadlockTimeout)
|
||||
fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", e.gid, e.ptr)
|
||||
printStack(Opts.LogBuf, e.stack)
|
||||
stacks := stacks()
|
||||
grs := bytes.Split(stacks, []byte("\n\n"))
|
||||
for _, prev := range holders {
|
||||
for _, g := range grs {
|
||||
if goid.ExtractGID(g) == prev.gid {
|
||||
fmt.Fprintln(Opts.LogBuf, "Here is what goroutine", prev.gid, "doing now")
|
||||
Opts.LogBuf.Write(g)
|
||||
fmt.Fprintln(Opts.LogBuf)
|
||||
}
|
||||
}
|
||||
}
|
||||
lo.other(e.ptr)
|
||||
if Opts.PrintAllCurrentGoroutines {
|
||||
fmt.Fprintln(Opts.LogBuf, "All current goroutines:")
|
||||
Opts.LogBuf.Write(stacks)
|
||||
}
|
||||
fmt.Fprintln(Opts.LogBuf)
|
||||
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
|
||||
buf.Flush()
|
||||
}
|
||||
Opts.mu.Unlock()
|
||||
lo.mu.Unlock()
|
||||
Opts.OnPotentialDeadlock()
|
||||
}
|
||||
|
||||
type lockOrder struct {
|
||||
mu sync.Mutex
|
||||
cur map[interface{}]stackGID // stacktraces + gids for the locks currently taken.
|
||||
order map[beforeAfter]ss // expected order of locks.
|
||||
cur map[interface{}][]stackGID // stacktraces + gids for the locks currently taken.
|
||||
order map[beforeAfter]ss // expected order of locks.
|
||||
}
|
||||
|
||||
type stackGID struct {
|
||||
stack []uintptr
|
||||
gid int64
|
||||
}
|
||||
|
||||
type beforeAfter struct {
|
||||
before interface{}
|
||||
after interface{}
|
||||
buf *[stackBufSize]uintptr // pooled backing array; returned via releaseStackBuf in postUnlock
|
||||
}
|
||||
|
||||
type ss struct {
|
||||
@@ -284,15 +378,31 @@ var lo = newLockOrder()
|
||||
|
||||
func newLockOrder() *lockOrder {
|
||||
return &lockOrder{
|
||||
cur: map[interface{}]stackGID{},
|
||||
cur: map[interface{}][]stackGID{},
|
||||
order: map[beforeAfter]ss{},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lockOrder) postLock(stack []uintptr, p interface{}) {
|
||||
// holdersPool recycles []stackGID slices used by lockOrder.cur to track which
|
||||
// goroutines currently hold each lock. Slices are returned to the pool in
|
||||
// postUnlock when a lock's holder count drops to zero, and reused in postLock
|
||||
// for the next lock acquisition, avoiding a new slice allocation per mutex.
|
||||
var holdersPool sync.Pool
|
||||
|
||||
// postLock records the current goroutine as a holder of lock p. It tries to
|
||||
// reuse a pooled []stackGID slice before allocating, and stores the pooled
|
||||
// stack buffer in the entry so postUnlock can release it later.
|
||||
func (l *lockOrder) postLock(stack []uintptr, buf *[stackBufSize]uintptr, p interface{}) {
|
||||
gid := goid.Get()
|
||||
entry := stackGID{stack, gid, buf}
|
||||
l.mu.Lock()
|
||||
l.cur[p] = stackGID{stack, gid}
|
||||
holders := l.cur[p]
|
||||
if holders == nil {
|
||||
if s, ok := holdersPool.Get().([]stackGID); ok {
|
||||
holders = s[:0]
|
||||
}
|
||||
}
|
||||
l.cur[p] = append(holders, entry)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -302,84 +412,131 @@ func (l *lockOrder) preLock(stack []uintptr, p interface{}) {
|
||||
}
|
||||
gid := goid.Get()
|
||||
l.mu.Lock()
|
||||
for b, bs := range l.cur {
|
||||
for b, holders := range l.cur {
|
||||
if b == p {
|
||||
if bs.gid == gid {
|
||||
for _, bs := range holders {
|
||||
if bs.gid == gid {
|
||||
Opts.mu.Lock()
|
||||
fmt.Fprintln(Opts.LogBuf, header, "Recursive locking:")
|
||||
fmt.Fprintf(Opts.LogBuf, "current goroutine %d lock %p\n", gid, b)
|
||||
printStack(Opts.LogBuf, stack)
|
||||
fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed (same goroutine)")
|
||||
printStack(Opts.LogBuf, bs.stack)
|
||||
l.other(p)
|
||||
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
|
||||
buf.Flush()
|
||||
}
|
||||
Opts.mu.Unlock()
|
||||
Opts.OnPotentialDeadlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, bs := range holders {
|
||||
if bs.gid != gid { // We want locks taken in the same goroutine only.
|
||||
continue
|
||||
}
|
||||
if s, ok := l.order[newBeforeAfter(p, b)]; ok {
|
||||
Opts.mu.Lock()
|
||||
fmt.Fprintln(Opts.LogBuf, header, "Recursive locking:")
|
||||
fmt.Fprintf(Opts.LogBuf, "current goroutine %d lock %p\n", gid, b)
|
||||
printStack(Opts.LogBuf, stack)
|
||||
fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed (same goroutine)")
|
||||
fmt.Fprintln(Opts.LogBuf, header, "Inconsistent locking. saw this ordering in one goroutine:")
|
||||
fmt.Fprintln(Opts.LogBuf, "happened before")
|
||||
printStack(Opts.LogBuf, s.before)
|
||||
fmt.Fprintln(Opts.LogBuf, "happened after")
|
||||
printStack(Opts.LogBuf, s.after)
|
||||
fmt.Fprintln(Opts.LogBuf, "in another goroutine: happened before")
|
||||
printStack(Opts.LogBuf, bs.stack)
|
||||
fmt.Fprintln(Opts.LogBuf, "happened after")
|
||||
printStack(Opts.LogBuf, stack)
|
||||
l.other(p)
|
||||
fmt.Fprintln(Opts.LogBuf)
|
||||
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
|
||||
buf.Flush()
|
||||
}
|
||||
Opts.mu.Unlock()
|
||||
Opts.OnPotentialDeadlock()
|
||||
}
|
||||
continue
|
||||
}
|
||||
if bs.gid != gid { // We want locks taken in the same goroutine only.
|
||||
continue
|
||||
}
|
||||
if s, ok := l.order[beforeAfter{p, b}]; ok {
|
||||
Opts.mu.Lock()
|
||||
fmt.Fprintln(Opts.LogBuf, header, "Inconsistent locking. saw this ordering in one goroutine:")
|
||||
fmt.Fprintln(Opts.LogBuf, "happened before")
|
||||
printStack(Opts.LogBuf, s.before)
|
||||
fmt.Fprintln(Opts.LogBuf, "happened after")
|
||||
printStack(Opts.LogBuf, s.after)
|
||||
fmt.Fprintln(Opts.LogBuf, "in another goroutine: happened before")
|
||||
printStack(Opts.LogBuf, bs.stack)
|
||||
fmt.Fprintln(Opts.LogBuf, "happened after")
|
||||
printStack(Opts.LogBuf, stack)
|
||||
l.other(p)
|
||||
fmt.Fprintln(Opts.LogBuf)
|
||||
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
|
||||
buf.Flush()
|
||||
// Copy both stacks: they're backed by pooled buffers that will be
|
||||
// recycled in postUnlock, but l.order entries persist until MaxMapSize.
|
||||
l.order[newBeforeAfter(b, p)] = ss{copyStack(bs.stack), copyStack(stack)}
|
||||
if len(l.order) == Opts.MaxMapSize { // Reset the map to keep memory footprint bounded.
|
||||
l.order = map[beforeAfter]ss{}
|
||||
}
|
||||
Opts.mu.Unlock()
|
||||
Opts.OnPotentialDeadlock()
|
||||
}
|
||||
l.order[beforeAfter{b, p}] = ss{bs.stack, stack}
|
||||
if len(l.order) == Opts.MaxMapSize { // Reset the map to keep memory footprint bounded.
|
||||
l.order = map[beforeAfter]ss{}
|
||||
}
|
||||
}
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *lockOrder) postUnlock(p interface{}) {
|
||||
gid := goid.Get()
|
||||
l.mu.Lock()
|
||||
delete(l.cur, p)
|
||||
holders := l.cur[p]
|
||||
idx := -1
|
||||
for i, h := range holders {
|
||||
if h.gid == gid {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx >= 0 {
|
||||
removedBuf := holders[idx].buf
|
||||
holders[idx] = holders[len(holders)-1]
|
||||
holders[len(holders)-1] = stackGID{}
|
||||
holders = holders[:len(holders)-1]
|
||||
releaseStackBuf(removedBuf)
|
||||
} else if len(holders) > 0 {
|
||||
// Cross-goroutine unlock: Go permits one goroutine to Lock and a different
|
||||
// goroutine to Unlock, so the unlocking gid may not match any holder entry.
|
||||
// This is a rare edge case in practice, the vast majority of code unlocks
|
||||
// from the same goroutine that locked. We remove an arbitrary entry to keep
|
||||
// the holder count consistent with the real lock state (the lock *was*
|
||||
// released, so one entry must go). The trade-off: for RWMutex with multiple
|
||||
// concurrent readers we may discard the wrong reader's stack trace, making a
|
||||
// future deadlock report show a slightly misleading "previous lock site".
|
||||
// Detection correctness is unaffected.
|
||||
removedBuf := holders[len(holders)-1].buf
|
||||
holders[len(holders)-1] = stackGID{}
|
||||
holders = holders[:len(holders)-1]
|
||||
releaseStackBuf(removedBuf)
|
||||
}
|
||||
if len(holders) == 0 {
|
||||
// Delete the map key so the mutex pointer is not retained, allowing GC of
|
||||
// the struct it's embedded in. Recycle the backing slice via pool so the
|
||||
// next postLock on any mutex can reuse it instead of allocating.
|
||||
if cap(holders) > 0 {
|
||||
holdersPool.Put(holders[:0])
|
||||
}
|
||||
delete(l.cur, p)
|
||||
} else {
|
||||
l.cur[p] = holders
|
||||
}
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
type rlocker RWMutex
|
||||
|
||||
func (r *rlocker) Lock() { (*RWMutex)(r).RLock() }
|
||||
func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() }
|
||||
|
||||
// Under lo.mu Locked.
|
||||
func (l *lockOrder) other(ptr interface{}) {
|
||||
empty := true
|
||||
for k := range l.cur {
|
||||
for k, holders := range l.cur {
|
||||
if k == ptr {
|
||||
continue
|
||||
}
|
||||
empty = false
|
||||
if len(holders) > 0 {
|
||||
empty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if empty {
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(Opts.LogBuf, "Other goroutines holding locks:")
|
||||
for k, pp := range l.cur {
|
||||
for k, holders := range l.cur {
|
||||
if k == ptr {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", pp.gid, k)
|
||||
printStack(Opts.LogBuf, pp.stack)
|
||||
for _, pp := range holders {
|
||||
fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", pp.gid, k)
|
||||
printStack(Opts.LogBuf, pp.stack)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(Opts.LogBuf)
|
||||
}
|
||||
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package deadlock
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package deadlock
|
||||
|
||||
import "sync"
|
||||
|
||||
// MutexImpl defines the interface for mutex implementations
|
||||
type MutexImpl interface {
|
||||
Lock()
|
||||
Unlock()
|
||||
TryLock() bool
|
||||
}
|
||||
|
||||
// RWMutexImpl defines the interface for rwmutex implementations
|
||||
type RWMutexImpl interface {
|
||||
Lock()
|
||||
Unlock()
|
||||
RLock()
|
||||
RUnlock()
|
||||
TryLock() bool
|
||||
TryRLock() bool
|
||||
RLocker() sync.Locker
|
||||
}
|
||||
+32
-3
@@ -13,9 +13,38 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
func callers(skip int) []uintptr {
|
||||
s := make([]uintptr, 50) // Most relevant context seem to appear near the top of the stack.
|
||||
return s[:runtime.Callers(2+skip, s)]
|
||||
const stackBufSize = 50
|
||||
|
||||
var stackBufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new([stackBufSize]uintptr)
|
||||
},
|
||||
}
|
||||
|
||||
// callers returns a stack trace backed by a pooled buffer. The caller must
|
||||
// eventually return buf via releaseStackBuf — typically through the
|
||||
// postLock/postUnlock path which stores it in stackGID.buf.
|
||||
func callers(skip int) ([]uintptr, *[stackBufSize]uintptr) {
|
||||
buf := stackBufPool.Get().(*[stackBufSize]uintptr)
|
||||
n := runtime.Callers(2+skip, buf[:])
|
||||
return buf[:n], buf
|
||||
}
|
||||
|
||||
// releaseStackBuf returns a pooled stack buffer obtained from callers(). Safe to
|
||||
// call with nil (e.g. when the buffer was already handed off via stackGID.buf).
|
||||
func releaseStackBuf(buf *[stackBufSize]uintptr) {
|
||||
if buf != nil {
|
||||
stackBufPool.Put(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// copyStack creates an independent copy of a stack trace. Required when storing
|
||||
// stacks in long-lived structures (e.g. l.order) because the originals are backed
|
||||
// by pooled buffers that will be recycled in postUnlock.
|
||||
func copyStack(s []uintptr) []uintptr {
|
||||
c := make([]uintptr, len(s))
|
||||
copy(c, s)
|
||||
return c
|
||||
}
|
||||
|
||||
func printStack(w io.Writer, stack []uintptr) {
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
//go:build deadlock_disable && !go1.18
|
||||
|
||||
package deadlock
|
||||
|
||||
import "sync"
|
||||
|
||||
// StandardMutex wraps sync.Mutex with no deadlock detection
|
||||
type StandardMutex struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (m *StandardMutex) Lock() {
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
func (m *StandardMutex) Unlock() {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *StandardMutex) TryLock() bool {
|
||||
panic("TryLock requires Go 1.18 or later")
|
||||
}
|
||||
|
||||
// StandardRWMutex wraps sync.RWMutex with no deadlock detection
|
||||
type StandardRWMutex struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) Lock() {
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) Unlock() {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RLock() {
|
||||
m.mu.RLock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RUnlock() {
|
||||
m.mu.RUnlock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) TryLock() bool {
|
||||
panic("TryLock requires Go 1.18 or later")
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) TryRLock() bool {
|
||||
panic("TryRLock requires Go 1.18 or later")
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RLocker() sync.Locker {
|
||||
return m.mu.RLocker()
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
//go:build deadlock_disable && go1.18
|
||||
|
||||
package deadlock
|
||||
|
||||
import "sync"
|
||||
|
||||
// StandardMutex wraps sync.Mutex with no deadlock detection
|
||||
type StandardMutex struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (m *StandardMutex) Lock() {
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
func (m *StandardMutex) Unlock() {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *StandardMutex) TryLock() bool {
|
||||
return m.mu.TryLock()
|
||||
}
|
||||
|
||||
// StandardRWMutex wraps sync.RWMutex with no deadlock detection
|
||||
type StandardRWMutex struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) Lock() {
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) Unlock() {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RLock() {
|
||||
m.mu.RLock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RUnlock() {
|
||||
m.mu.RUnlock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) TryLock() bool {
|
||||
return m.mu.TryLock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) TryRLock() bool {
|
||||
return m.mu.TryRLock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RLocker() sync.Locker {
|
||||
return m.mu.RLocker()
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
//go:build !goexperiment.synctest && !deadlock_synctest && !deadlock_disable && go1.18
|
||||
// +build !goexperiment.synctest,!deadlock_synctest,!deadlock_disable,go1.18
|
||||
|
||||
package deadlock
|
||||
|
||||
import "sync"
|
||||
|
||||
// StandardMutex wraps sync.Mutex
|
||||
type StandardMutex struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (m *StandardMutex) Lock() {
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
func (m *StandardMutex) Unlock() {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *StandardMutex) TryLock() bool {
|
||||
return m.mu.TryLock()
|
||||
}
|
||||
|
||||
// StandardRWMutex wraps sync.RWMutex
|
||||
type StandardRWMutex struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) Lock() {
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) Unlock() {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RLock() {
|
||||
m.mu.RLock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RUnlock() {
|
||||
m.mu.RUnlock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) TryLock() bool {
|
||||
return m.mu.TryLock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) TryRLock() bool {
|
||||
return m.mu.TryRLock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RLocker() sync.Locker {
|
||||
return m.mu.RLocker()
|
||||
}
|
||||
|
||||
// Default factory functions
|
||||
func newStandardMutex() MutexImpl {
|
||||
return &StandardMutex{}
|
||||
}
|
||||
|
||||
func newStandardRWMutex() RWMutexImpl {
|
||||
return &StandardRWMutex{}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
//go:build !goexperiment.synctest && !deadlock_synctest && !deadlock_disable && !go1.18
|
||||
// +build !goexperiment.synctest,!deadlock_synctest,!deadlock_disable,!go1.18
|
||||
|
||||
package deadlock
|
||||
|
||||
import "sync"
|
||||
|
||||
// StandardMutex wraps sync.Mutex
|
||||
type StandardMutex struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (m *StandardMutex) Lock() {
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
func (m *StandardMutex) Unlock() {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *StandardMutex) TryLock() bool {
|
||||
// TryLock is not available before Go 1.18
|
||||
panic("TryLock requires Go 1.18 or later")
|
||||
}
|
||||
|
||||
// StandardRWMutex wraps sync.RWMutex
|
||||
type StandardRWMutex struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) Lock() {
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) Unlock() {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RLock() {
|
||||
m.mu.RLock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RUnlock() {
|
||||
m.mu.RUnlock()
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) TryLock() bool {
|
||||
// TryLock is not available before Go 1.18
|
||||
panic("TryLock requires Go 1.18 or later")
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) TryRLock() bool {
|
||||
// TryRLock is not available before Go 1.18
|
||||
panic("TryRLock requires Go 1.18 or later")
|
||||
}
|
||||
|
||||
func (m *StandardRWMutex) RLocker() sync.Locker {
|
||||
return m.mu.RLocker()
|
||||
}
|
||||
|
||||
// Default factory functions
|
||||
func newStandardMutex() MutexImpl {
|
||||
return &StandardMutex{}
|
||||
}
|
||||
|
||||
func newStandardRWMutex() RWMutexImpl {
|
||||
return &StandardRWMutex{}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
//go:build (goexperiment.synctest || deadlock_synctest) && !deadlock_disable
|
||||
|
||||
package deadlock
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// ChannelMutex implements MutexImpl using channels for synctest compatibility
|
||||
type ChannelMutex struct {
|
||||
ch chan struct{}
|
||||
locked int32 // atomic
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (m *ChannelMutex) init() {
|
||||
m.once.Do(func() {
|
||||
m.ch = make(chan struct{}, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *ChannelMutex) Lock() {
|
||||
m.init()
|
||||
m.ch <- struct{}{}
|
||||
atomic.StoreInt32(&m.locked, 1)
|
||||
}
|
||||
|
||||
func (m *ChannelMutex) Unlock() {
|
||||
if atomic.LoadInt32(&m.locked) == 0 {
|
||||
panic("unlock of unlocked mutex")
|
||||
}
|
||||
atomic.StoreInt32(&m.locked, 0)
|
||||
<-m.ch
|
||||
}
|
||||
|
||||
func (m *ChannelMutex) TryLock() bool {
|
||||
m.init()
|
||||
select {
|
||||
case m.ch <- struct{}{}:
|
||||
atomic.StoreInt32(&m.locked, 1)
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ChannelRWMutex implements RWMutexImpl with writer priority using channels.
|
||||
// Implements writer-priority semantics using the "Third Readers-Writers Problem" solution.
|
||||
type ChannelRWMutex struct {
|
||||
resource chan struct{} // The actual resource being protected
|
||||
readTry chan struct{} // Gate that closes when writers waiting
|
||||
rmutex chan struct{} // Protects readCount modifications
|
||||
wmutex chan struct{} // Protects writeCount modifications
|
||||
readCount int32 // Number of active readers
|
||||
writeCount int32 // Number of waiting/active writers
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (m *ChannelRWMutex) init() {
|
||||
m.once.Do(func() {
|
||||
m.resource = make(chan struct{}, 1)
|
||||
m.readTry = make(chan struct{}, 1)
|
||||
m.rmutex = make(chan struct{}, 1)
|
||||
m.wmutex = make(chan struct{}, 1)
|
||||
// Initially, all semaphores are "released" (have a token)
|
||||
m.resource <- struct{}{}
|
||||
m.readTry <- struct{}{}
|
||||
m.rmutex <- struct{}{}
|
||||
m.wmutex <- struct{}{}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *ChannelRWMutex) Lock() {
|
||||
m.init()
|
||||
// Protect writeCount modification
|
||||
<-m.wmutex
|
||||
count := atomic.AddInt32(&m.writeCount, 1)
|
||||
if count == 1 {
|
||||
// First writer: close the gate to block new readers
|
||||
<-m.readTry
|
||||
}
|
||||
m.wmutex <- struct{}{} // Release wmutex
|
||||
|
||||
// Acquire the resource (wait for existing readers to finish)
|
||||
<-m.resource
|
||||
}
|
||||
|
||||
func (m *ChannelRWMutex) Unlock() {
|
||||
// Release the resource
|
||||
m.resource <- struct{}{}
|
||||
|
||||
// Protect writeCount modification
|
||||
<-m.wmutex
|
||||
count := atomic.AddInt32(&m.writeCount, -1)
|
||||
if count == 0 {
|
||||
// Last writer: reopen the gate for readers
|
||||
m.readTry <- struct{}{}
|
||||
}
|
||||
m.wmutex <- struct{}{}
|
||||
}
|
||||
|
||||
func (m *ChannelRWMutex) RLock() {
|
||||
m.init()
|
||||
// Wait at the gate (blocks if writers are waiting)
|
||||
<-m.readTry
|
||||
|
||||
// Protect readCount modification
|
||||
<-m.rmutex
|
||||
count := atomic.AddInt32(&m.readCount, 1)
|
||||
if count == 1 {
|
||||
// First reader: acquire the resource to block writers
|
||||
<-m.resource
|
||||
}
|
||||
m.rmutex <- struct{}{} // Release rmutex
|
||||
|
||||
// Release the gate so other readers can pass
|
||||
m.readTry <- struct{}{}
|
||||
}
|
||||
|
||||
func (m *ChannelRWMutex) RUnlock() {
|
||||
<-m.rmutex
|
||||
count := atomic.AddInt32(&m.readCount, -1)
|
||||
if count < 0 {
|
||||
m.rmutex <- struct{}{}
|
||||
panic("RUnlock of unlocked RWMutex")
|
||||
}
|
||||
if count == 0 {
|
||||
// Last reader: release the resource for writers
|
||||
m.resource <- struct{}{}
|
||||
}
|
||||
m.rmutex <- struct{}{}
|
||||
}
|
||||
|
||||
func (m *ChannelRWMutex) TryLock() bool {
|
||||
m.init()
|
||||
// Try to acquire wmutex
|
||||
select {
|
||||
case <-m.wmutex:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
count := atomic.AddInt32(&m.writeCount, 1)
|
||||
if count == 1 {
|
||||
// First writer: try to close gate
|
||||
select {
|
||||
case <-m.readTry:
|
||||
default:
|
||||
// Failed, rollback
|
||||
atomic.AddInt32(&m.writeCount, -1)
|
||||
m.wmutex <- struct{}{}
|
||||
return false
|
||||
}
|
||||
}
|
||||
m.wmutex <- struct{}{}
|
||||
|
||||
// Try to acquire resource
|
||||
select {
|
||||
case <-m.resource:
|
||||
return true
|
||||
default:
|
||||
// Failed, rollback writer count
|
||||
<-m.wmutex
|
||||
count = atomic.AddInt32(&m.writeCount, -1)
|
||||
if count == 0 {
|
||||
m.readTry <- struct{}{}
|
||||
}
|
||||
m.wmutex <- struct{}{}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ChannelRWMutex) TryRLock() bool {
|
||||
m.init()
|
||||
// Try to pass through the gate
|
||||
select {
|
||||
case <-m.readTry:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to acquire rmutex
|
||||
select {
|
||||
case <-m.rmutex:
|
||||
default:
|
||||
// Failed, release gate
|
||||
m.readTry <- struct{}{}
|
||||
return false
|
||||
}
|
||||
|
||||
count := atomic.AddInt32(&m.readCount, 1)
|
||||
if count == 1 {
|
||||
// First reader: try to acquire resource
|
||||
select {
|
||||
case <-m.resource:
|
||||
default:
|
||||
// Failed, rollback
|
||||
atomic.AddInt32(&m.readCount, -1)
|
||||
m.rmutex <- struct{}{}
|
||||
m.readTry <- struct{}{}
|
||||
return false
|
||||
}
|
||||
}
|
||||
m.rmutex <- struct{}{}
|
||||
m.readTry <- struct{}{}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *ChannelRWMutex) RLocker() sync.Locker {
|
||||
return (*channelRLocker)(m)
|
||||
}
|
||||
|
||||
type channelRLocker ChannelRWMutex
|
||||
|
||||
func (r *channelRLocker) Lock() { (*ChannelRWMutex)(r).RLock() }
|
||||
func (r *channelRLocker) Unlock() { (*ChannelRWMutex)(r).RUnlock() }
|
||||
|
||||
// Factory functions for synctest
|
||||
func newChannelMutex() MutexImpl {
|
||||
return &ChannelMutex{
|
||||
ch: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func newChannelRWMutex() RWMutexImpl {
|
||||
m := &ChannelRWMutex{}
|
||||
m.init()
|
||||
return m
|
||||
}
|
||||
|
||||
// Type aliases to override the standard mutex types for synctest
|
||||
type StandardMutex = ChannelMutex
|
||||
type StandardRWMutex = ChannelRWMutex
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
//go:build !goexperiment.synctest && !deadlock_synctest && !deadlock_disable && !go1.25
|
||||
|
||||
package deadlock
|
||||
|
||||
// shouldDisableTimerPool determines if timer pooling should be disabled
|
||||
// In normal builds, timer pooling is enabled by default for performance
|
||||
func shouldDisableTimerPool() bool {
|
||||
switch Opts.TimerPool {
|
||||
case TimerPoolDefault:
|
||||
return false // Default: enable timer pooling for performance
|
||||
case TimerPoolEnabled:
|
||||
return false
|
||||
case TimerPoolDisabled:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
//go:build deadlock_disable
|
||||
|
||||
package deadlock
|
||||
|
||||
// shouldDisableTimerPool always returns true when deadlock detection is disabled
|
||||
// since there's no timer pool or deadlock detection happening anyway
|
||||
func shouldDisableTimerPool() bool {
|
||||
return true
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
//go:build !goexperiment.synctest && !deadlock_synctest && !deadlock_disable && go1.25
|
||||
|
||||
package deadlock
|
||||
|
||||
// shouldDisableTimerPool determines if timer/entry pooling should be disabled.
|
||||
// In Go 1.25, pooling is enabled by default for performance.
|
||||
func shouldDisableTimerPool() bool {
|
||||
switch Opts.TimerPool {
|
||||
case TimerPoolDefault:
|
||||
return false // Default: enable timer pooling for performance
|
||||
case TimerPoolEnabled:
|
||||
return false
|
||||
case TimerPoolDisabled:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
//go:build (goexperiment.synctest || deadlock_synctest) && !deadlock_disable
|
||||
|
||||
package deadlock
|
||||
|
||||
// shouldDisableTimerPool determines if timer pooling should be disabled
|
||||
// In synctest builds, timer pooling is disabled by default to avoid cross-bubble issues
|
||||
func shouldDisableTimerPool() bool {
|
||||
switch Opts.TimerPool {
|
||||
case TimerPoolDefault:
|
||||
return true // Default: disable timer pooling for synctest compatibility
|
||||
case TimerPoolEnabled:
|
||||
return false
|
||||
case TimerPoolDisabled:
|
||||
return true
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -1,3 +1,4 @@
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package deadlock
|
||||
@@ -27,13 +28,16 @@ func trylock(lockFn func() bool, ptr interface{}) bool {
|
||||
if Opts.Disable {
|
||||
return lockFn()
|
||||
}
|
||||
stack := callers(1)
|
||||
stack, buf := callers(1)
|
||||
preLock(stack, ptr)
|
||||
ret := lockFn()
|
||||
if ret {
|
||||
postLock(stack, ptr)
|
||||
postLock(stack, buf, ptr)
|
||||
} else {
|
||||
// TryLock failed: the stack won't be stored in stackGID.buf (postLock is
|
||||
// skipped), so we must release the pooled buffer directly to avoid a leak.
|
||||
releaseStackBuf(buf)
|
||||
postUnlock(ptr)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -165,7 +165,7 @@ github.com/samber/lo
|
||||
# github.com/sanity-io/litter v1.5.8
|
||||
## explicit; go 1.16
|
||||
github.com/sanity-io/litter
|
||||
# github.com/sasha-s/go-deadlock v0.3.6
|
||||
# github.com/sasha-s/go-deadlock v0.3.9
|
||||
## explicit
|
||||
github.com/sasha-s/go-deadlock
|
||||
# github.com/sirupsen/logrus v1.9.3
|
||||
|
||||
Reference in New Issue
Block a user