Concurrency In Go
The programming launguage Go has provided a different way of solving the concurrent programming problem. It has the goroutin which is similar to thread. Go’s concurrency model is built on the philosophy: “Do not communicate by sharing memory; instead, share memory by communicating.”
Start with an example
package main
import "time"
import "math/rand"
func main() {
rand.Seed(time.Now().UnixNano())
count := 0
finished := 0
for i := 0; i < 10; i++ {
go func() {
vote := requestVote()
if vote {
count++
}
finished++
}()
}
for count < 5 && finished != 10 {
// wait
}
if count >= 5 {
println("received 5+ votes!")
} else {
println("lost")
}
}
func requestVote() bool {
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
return rand.Int() % 2 == 0
}
The above code snippet doesn’t have synchronization which cause incorrect output generated. If you run it, you’ll see both “received 5+ votes!” and “lost”
show up as the result. This is caused by the fact that the variable count and finished are shared by different goroutines.
Solve It With Locking
package main
import "sync"
import "time"
import "math/rand"
func main() {
rand.Seed(time.Now().UnixNano())
count := 0
finished := 0
var mu sync.Mutex
for i := 0; i < 10; i++ {
go func() {
vote := requestVote()
mu.Lock()
defer mu.Unlock()
if vote {
count++
}
finished++
}()
}
for {
mu.Lock()
if count >= 5 || finished == 10 {
break
}
mu.Unlock()
}
if count >= 5 {
println("received 5+ votes!")
} else {
println("lost")
}
mu.Unlock()
}
The obvious solution is to add lock and wrap it around the two variables. This works however it’s inefficient. The reason is because when the main thread acquires the lock, the other thread need to wait. And the main thread will continuous attempting to acquires the lock which will cause the CPU wasted.
Condition Variable
package main
import "sync"
import "time"
import "math/rand"
func main() {
rand.Seed(time.Now().UnixNano())
count := 0
finished := 0
var mu sync.Mutex
cond := sync.NewCond(&mu)
for i := 0; i < 10; i++ {
go func() {
vote := requestVote()
mu.Lock()
defer mu.Unlock()
if vote {
count++
}
finished++
cond.Broadcast()
}()
}
mu.Lock()
for count < 5 && finished != 10 {
cond.Wait()
}
if count >= 5 {
println("received 5+ votes!")
} else {
println("lost")
}
mu.Unlock()
}
A better solution is to utilize the condition variable. In the example code, we declare a condition variable cond which contains a reference to the
lock mu. This way when the count and finished variable doesn’t meet the condition yet, it’ll release the mu lock first and then sleep. It’ll wake
up when one of the goroutines finishes and receive the broadcast. After that, the main thread will acquire the lock again and check again to see if it
start to meet the requirement or not. As you can see, now there is a coordination between main routin and the sub-routine.
Go Channel To Solve it
package main
import "time"
import "math/rand"
func main() {
rand.Seed(time.Now().UnixNano())
count := 0
finished := 0
ch := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
ch <- requestVote()
}()
}
for count < 5 && finished < 10 {
v := <-ch
if v {
count += 1
}
finished += 1
}
if count >= 5 {
println("received 5+ votes!")
} else {
println("lost")
}
}
Go provides a concept called channel so that the routines can coordinate with each other. In this case, we declares a channel called ch here. The main
thread will block when it tries to retrieve value from the channel. In the code, it’s indicated as v := <-ch. Whenever any goroutines send a value to the
channel: ch <- requestVote(), the main thread will resume execution. As you can see, this has solved the concurrency issues.