Closure goroutine takes unexpected value from loop iterator

0x00 intro

Let’s take a look at the code below. It’s a typical iterator-based loop in go, with a closure function being run as a goroutine in the loop body.

for i := 0; i <= 9; i++ {
	go func() {

Consider what is the output of this code. Is it “0” to “9” in random order? To my surprise, the answer is no. The actual output of this code is ten “10” in most of the time.

I firstly learn this from a talk from GopherCon UK 2018 named as The Dark Side of the Runtime. And today I actually meet this issue by accident. It’s impressive.

0x01 Problem

Today, I’m about to add some new feature onto my new go project as usual. I run go run to launch the application. Then an exception being thrown from somewhere far away from my new changes. And it says: unlock of unlocked mutex.

fatal error: sync: unlock of unlocked mutex

goroutine 38 [running]:
runtime.throw(0xc85355, 0x1e)
	/usr/lib/golang/src/runtime/panic.go:608 +0x72 fp=0xc0034cbf20 sp=0xc0034cbef0 pc=0x42ce42
sync.throw(0xc85355, 0x1e)
	/usr/lib/golang/src/runtime/panic.go:594 +0x35 fp=0xc0034cbf40 sp=0xc0034cbf20 pc=0x42cdc5
	/usr/lib/golang/src/sync/mutex.go:184 +0xc1 fp=0xc0034cbf68 sp=0xc0034cbf40 pc=0x46fb21
github.com/lentil1016/numpicker/pkg/store.(*cache).heavyRank.func1(0xc0034b41e0, 0xc0001b4028, 0xc0001f4630, 0xc0002763b0)
	/root/.go/src/github.com/lentil1016/numpicker/pkg/store/advise.go:77 +0xbb fp=0xc0034cbfc0 sp=0xc0034cbf68 pc=0xaaf88b
	/usr/lib/golang/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc0034cbfc8 sp=0xc0034cbfc0 pc=0x45a191
created by github.com/lentil1016/numpicker/pkg/store.(*cache).heavyRank
	/root/.go/src/github.com/lentil1016/numpicker/pkg/store/advise.go:70 +0x123
func (c *cache) heavyRank() {
	var wg sync.WaitGroup
	for tag, mutex := range c.numberListMutexMap {
		go func() {
			defer wg.Done()
			if list, ok := c.numberListMap[tag]; ok {
				selector.ScoreNumbers(c.pool.Get(), list)

This is the code that throw the exception.

At first I was totally confused, I double checked my code in the critical section, there is only one Lock and one Unlock calling, no branches, no return before Unlock being called, there just no way a mutex can be unlocked for twice.

But you realized something, don’t you? Yes, this is exactly the same issue described at the beginning of this post. Somehow mutex in the closure received a unexpected value from iterator.

0x02 why?

In iteration-based loop, iterator is a local variable, this variable will be assigned in the beginning of every looping. Then the closure takes that variable itself (like pass-by-reference in C++), and create a goroutine, waiting for being scheduled to run.

Everything looks fine except the actual value of iterator is still changing!

That means each goroutine that run from inside this loop will read value from exactly the same memory address that might still being constantly assigned from looping, even if it’s not, the value in it is not what we are expecting.

Finally, let’s go back to my example. You can imagine, After one goroutine grab the mutex and lock it, there is a small chance that the value of mutex will be iterated to the next element before the Unlock is being called. Which means that Unlock will be applied to a different mutex. Which ultimately caused this exception.

0x03 Conclusion

To avoid this, just simply add a local variable to store the current value of the iterator.

for i := 0; i <= 9; i++ {
	i := i
	go func() {

BTW, The Dark Side of the Runtime is really an inspiring talk.

分类: go
文章已创建 23


您的电子邮箱地址不会被公开。 必填项已用 * 标注