New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/vet: add check for sync.WaitGroup abuse #18022

Open
dsnet opened this Issue Nov 22, 2016 · 17 comments

Comments

Projects
None yet
8 participants
@dsnet
Member

dsnet commented Nov 22, 2016

The API for WaitGroup is very easy for people to misuse. The documentation for Add says:

calls to Add should execute before the statement creating the goroutine or other event to be waited for.

However, it is very common to see this incorrect pattern:

var wg sync.WaitGroup
defer wg.Wait()

go func() {
	wg.Add(1)
	defer wg.Done()
}()

This usage is fundamentally racy and probably does not do what the user wanted. Worse yet, is that it is not detectable by the race detector.

Since the above pattern is common, I propose that we add a method Go that essentially does the Add(1) and subsequent call to Done in the correct way. That is:

func (wg *WaitGroup) Go(f func()) {
	wg.Add(1)
	go func() {
		defer wg.Done()
		f()
	}()
}

@dsnet dsnet added the Proposal label Nov 22, 2016

@dsnet dsnet added this to the Proposal milestone Nov 22, 2016

@cznic

This comment has been minimized.

Contributor

cznic commented Nov 23, 2016

I don't like the idea. It's IMHO perhaps a task for go vet, if not implemented already. Also, the proposed method would add a(nother) closure layer when the go-ed function has parameters.

@minux

This comment has been minimized.

Member

minux commented Nov 23, 2016

@dsnet

This comment has been minimized.

Member

dsnet commented Nov 23, 2016

A go vet check seems pretty reasonable. I just tried it right now on the following:

func main() {
	var wg sync.WaitGroup
	defer wg.Wait()
	go func() {
		wg.Add(1)
		defer wg.Done()
	}()
}

and vet doesn't report anything.

In terms of vet's requirements:

  • Correctness: The problem is clearly a race.
  • Frequency: My gut feeling is that this is fairly common. A number of internal bugs that I've fixed is of this nature. So it subjective feels common enough. I don't have hard numbers.
  • Precision: I don't have an algorithm in mind that can accurately identify the pattern and I can imagine some number of false positives.

@dominikh , staticcheck does report a problem:
main.go:10:3: should call wg.Add(1) before starting the goroutine to avoid a race (SA2000)
I'm wondering how accurate the check is and whether it is worth adding to vet.

@mvdan

This comment has been minimized.

Member

mvdan commented Nov 23, 2016

As far as the proposal goes, I don't like how it limits the func signature to func().

@dominikh

This comment has been minimized.

Member

dominikh commented Nov 23, 2016

@dsnet The check in staticcheck has no (known) false positives. It shouldn't have a significant number of false negatives, either. The implementation is a simple pattern-based check, detecting go with function literals where the first statement is a call to wg.Add – this avoids flagging wg.Add calls further down the goroutine, which tend to be valid uses.

I'm -1 on the proposed Go function. I'd prefer not having to read code with an unnecessary level of nesting that looks callback-esque and reminds me of JavaScript.

@mvdan

This comment has been minimized.

Member

mvdan commented Nov 23, 2016

@dominikh I don't see any extra level of nesting here, though (assuming any func signature is allowed).

To be nitpicky, another thing that stands out from the proposal is how wg.Go() will create a goroutine even though go is never directly used by the user. I don't know if the standard library does this anywhere else, but I would prefer if it was left explicit.

@dominikh

This comment has been minimized.

Member

dominikh commented Nov 23, 2016

@mvdan The extra level of nesting would come from a predicted usage that looks something like this:

wg.Go(func() {
  // do stuff
})

as opposed to

go func() {
  // do stuff
}()

Admittedly the same level of indentation, but syntactically it's one extra level of nesting.

@mvdan

This comment has been minimized.

Member

mvdan commented Nov 23, 2016

Ah yes, I was thinking indentation there.

@cznic

This comment has been minimized.

Contributor

cznic commented Nov 23, 2016

The problematic case is that

wg.Add(1)
go func(i int) { ... }(42)

// becomes

wg.Go(func() {
        go func(i int) { ... }(42)
})
@mvdan

This comment has been minimized.

Member

mvdan commented Nov 23, 2016

@cznic if you mean without the extra go, this would be solved if the restriction on the func() signature was removed.

@dominikh

This comment has been minimized.

Member

dominikh commented Nov 23, 2016

@mvdan Do you mean by allowing something like the following?

wg.Go(func(x, y int) { ...}, v1, v2)

IMHO that's way too much interface{} and not enough type safety.

@mattn

This comment has been minimized.

Member

mattn commented Nov 23, 2016

panic is recovered?

@mvdan

This comment has been minimized.

Member

mvdan commented Nov 23, 2016

@dominikh true; I was simply pointing at the issue without contemplating a solution :)

@rsc

This comment has been minimized.

Contributor

rsc commented Nov 28, 2016

The API change here has the problems identified above with argument evaluation. Also, in general the libraries do not typically phrase functionality in terms of callbacks. If we're going to start using callbacks broadly, that should be a separate decision (and not one to make today). For both these reasons, it seems like .Go is not a clear win.

It would be nice to have a vet check that we trust (no false positives). Perhaps it is enough to look for two statements

wg.Add(1)
defer wg.Done()

back to back and reject that always. Thoughts about how to make vet catch this reliably?

@renannprado

This comment has been minimized.

renannprado commented Dec 4, 2016

I agree that vet is better place for this. The proposed API reminds of JavaScript, which will force us many times to wrap the code or function within a function with no arguments, while you could have just go func()....
Still nothing can stop you from creating such a helper methid, even though I don't see the need for it.

@rsc

This comment has been minimized.

Contributor

rsc commented Dec 5, 2016

It sounds like we are deciding to make go vet check this and not add new API here. Any arguments against that?

@dsnet

This comment has been minimized.

Member

dsnet commented Dec 5, 2016

SGTM

@dsnet dsnet changed the title from proposal: sync: add Go method to WaitGroup to cmd/vet: add check for sync.WaitGroup abuse Dec 5, 2016

@dsnet dsnet removed the Proposal label Dec 9, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment