The most simple implementation of locker is sync.Mutex. Since its method has a pointer receiver, it should not be copied or passed around by value. The Lock() method takes control of the mutex if possible, or blocks the goroutine until the mutex becomes available. The Unlock() method releases the mutex and it returns a runtime error if called on a non-locked one.
Here is a simple example in which we launch a bunch of goroutines using the lock to see which is executed first:
func main() {
var m sync.Mutex
done := make(chan struct{}, 10)
for i := 0; i < cap(done); i++ {
go func(i int, l sync.Locker) {
l.Lock()
defer l.Unlock()
fmt.Println(i)
time.Sleep(time.Millisecond * 10)
done <- struct{}{}
}(i, &m)
}
for i := 0; i < cap(done); i++ {
<-done
}
}
The full example is available at: https://play.golang.org/p/resVh7LImLf
We are using a channel to signal the main goroutine when a job is done, and exit the application. Let's create an external counter and increment it concurrently using goroutines.
Operations executed on different goroutines are not thread-safe, as we can see from the following example:
done := make(chan struct{}, 10000)
var a = 0
for i := 0; i < cap(done); i++ {
go func(i int) {
if i%2 == 0 {
a++
} else {
a--
}
done <- struct{}{}
}(i)
}
for i := 0; i < cap(done); i++ {
<-done
}
fmt.Println(a)
We would expect to have 5000 plus one, and 5000 minus one, with a 0 printed in the final instruction. However, what we get are different values each time we run the application. This happens because these kind of operations are not thread-safe, so two or more of them could happen at the same time, with the last one shadowing the others. This kind of phenomena is known as a race condition; that is, when more than one operation is trying to write the same result.
This means that without any synchronization, the result is not predictable; if we check the previous example and use a lock to avoid the race condition, we will have zero as the value for the integer—the result that we were expecting:
m := sync.Mutex{}
for i := 0; i < cap(done); i++ {
go func(l sync.Locker, i int) {
l.Lock()
defer l.Unlock()
if i%2 == 0 {
a++
} else {
a--
}
done <- struct{}{}
}(&m, i)
fmt.Println(a)
}
A very common practice is embedding a mutex in a data structure to symbolize that the container is the one you want to lock. The counter variable from before can be represented as follows:
type counter struct {
m sync.Mutex
value int
}
The operations that the counter performs can be methods that already take care of locking before the main operation, along with unlocking it afterward, as shown in the following code block:
func (c *counter) Incr(){
c.m.Lock()
c.value++
c.m.Unlock()
}
func (c *counter) Decr(){
c.m.Lock()
c.value--
c.m.Unlock()
}
func (c *counter) Value() int {
c.m.Lock()
a := c.value
c.m.Unlock()
return a
}
This will simplify the goroutine loop, resulting in a much clearer code:
var a = counter{}
for i := 0; i < cap(done); i++ {
go func(i int) {
if i%2 == 0 {
a.Incr()
} else {
a.Decr()
}
done <- struct{}{}
}(i)
}
// ...
fmt.Println(a.Value())