mirror of https://github.com/golang/go.git
internal/singleflight: avoid race between multiple Do calls
When the first call to Do finished, it calls c.wg.Done() to signal others that the call was done. However, that happens without holding a lock, so if others call to Do complete and be followed by a call to ForgotUnshared, that then returns false. Fixing this by moving c.wg.Done() inside the section guarded by g.mu, so the two operations won't be interrupted. Thanks bcmills@ for finding and suggesting fix. Change-Id: I850f5eb3f9751a0aaa65624d4109aeeb59dee42c Reviewed-on: https://go-review.googlesource.com/c/go/+/436437 Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Bryan Mills <bcmills@google.com> Run-TryBot: Cuong Manh Le <cuong.manhle.vn@gmail.com> TryBot-Result: Gopher Robot <gobot@golang.org> Auto-Submit: Cuong Manh Le <cuong.manhle.vn@gmail.com>
This commit is contained in:
parent
1e65fa58c1
commit
aeedb5ab13
|
|
@ -91,9 +91,9 @@ func (g *Group) DoChan(key string, fn func() (any, error)) <-chan Result {
|
||||||
// doCall handles the single call for a key.
|
// doCall handles the single call for a key.
|
||||||
func (g *Group) doCall(c *call, key string, fn func() (any, error)) {
|
func (g *Group) doCall(c *call, key string, fn func() (any, error)) {
|
||||||
c.val, c.err = fn()
|
c.val, c.err = fn()
|
||||||
c.wg.Done()
|
|
||||||
|
|
||||||
g.mu.Lock()
|
g.mu.Lock()
|
||||||
|
c.wg.Done()
|
||||||
if g.m[key] == c {
|
if g.m[key] == c {
|
||||||
delete(g.m, key)
|
delete(g.m, key)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,3 +141,46 @@ func TestForgetUnshared(t *testing.T) {
|
||||||
t.Errorf("We should receive result produced by second call, expected: 2, got %d", result.Val)
|
t.Errorf("We should receive result produced by second call, expected: 2, got %d", result.Val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDoAndForgetUnsharedRace(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var g Group
|
||||||
|
key := "key"
|
||||||
|
d := time.Millisecond
|
||||||
|
for {
|
||||||
|
var calls, shared atomic.Int64
|
||||||
|
const n = 1000
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
go func() {
|
||||||
|
g.Do(key, func() (interface{}, error) {
|
||||||
|
time.Sleep(d)
|
||||||
|
return calls.Add(1), nil
|
||||||
|
})
|
||||||
|
if !g.ForgetUnshared(key) {
|
||||||
|
shared.Add(1)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if calls.Load() != 1 {
|
||||||
|
// The goroutines didn't park in g.Do in time,
|
||||||
|
// so the key was re-added and may have been shared after the call.
|
||||||
|
// Try again with more time to park.
|
||||||
|
d *= 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// All of the Do calls ended up sharing the first
|
||||||
|
// invocation, so the key should have been unused
|
||||||
|
// (and therefore unshared) when they returned.
|
||||||
|
if shared.Load() > 0 {
|
||||||
|
t.Errorf("after a single shared Do, ForgetUnshared returned false %d times", shared.Load())
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue