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.

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.

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.

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

分类: go
文章已创建 13


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