Today I went to Seattle office and took a traning course about Go Concurrency. It was a good opportunity for me to learn how it works and design principles behind. In my daily work, I know how it works vaguely. I am able to read the code and imitate. But I don’t know the ins and outs.
So I decided to write this series to help myself master this piece of knowledge. After all, concurrency is one of best features that Golang provides.
Goroutine
A goroutine is a light weight of thread execution.
go func(){
fmt.Println("Hello World!")
}()
Channel
Let’s imagine each goroutine is in a separate thread. How to communicate between threads? Golang provides Channels.
By default the goroutine communication is synchronous and unbuffered: sends do not complete until there is a receiver to accept the value. There must be a receiver ready to receive data from the channel and then the sender can hand it over directly to the receiver.
So channel send/receive operations block until the other side is ready:
A send operation on a channel blocks until a receiver is available for the same channel: if there’s no recipient for the value on ch, no other value can be put in the channel. And the other way around: no new value can be sent in ch when the channel is not empty! So the send operation will wait until ch becomes available again.
A receive operation for a channel blocks until a sender is available for the same channel: if there is no value in the channel, the receiver blocks.
func massiveWork() int {
time.Sleep(time.Duration(1) * time.Second)
return rand.Int()
}
func main() {
// Without concurrency
now := time.Now().UTC()
for i = 0; i < 5; i++ {
fmt.Println(massiveWork())
}
fmt.Println("Without concurrency:", time.Since(now))
// With concurrency
now = time.Now().UTC()
ch := make(chan int)
defer close(ch)
for i = 0; i < 5; i++ {
go func() {
ch <- massiveWork()
}()
}
for i = 0; i < 5; i++ {
fmt.Println(<- ch)
}
fmt.Println("With concurrency:", time.Since(now))
}
The result shows that without concurrency, it took 5s. With concurrency, it took 1s.
Timeout
Using goruntine is similar to assigning a task to a worker. Usually the boss is impatient and wants the task to be done within X minutes. Otherwise, the boss will be pissed off. Suppose the task has a timeout of 5s. If the worker cannot complete the task within 5s, it will stop working.
func main() {
ch := make(chan int)
go func(){
t := 12
time.Sleep(time.Duration(t) * time.Second)
ch <- 4
}()
select {
case n := <- ch:
fmt.Println("Work done", n)
return
case <- time.After(5 * time.Second):
fmt.Println("Stop!")
return
}
}
Pick the fastest
Besides timeout, the greedy boss assigned the same task to different workers. Only the worker who completes the task first will be paid. The loser will get nothing.
func main() {
ch := make(chan int)
go worker1(ch)
go worker2(ch)
go worker3(ch)
select {
case n := <- ch:
fmt.Println("Work done from worker#", n)
return
case <- time.After(3 * time.Second):
fmt.Println("Stop!")
return
}
}
func worker1(ch chan int) {
time.Sleep(1 * time.Second)
ch <- 1
}
func worker2(ch chan int) {
time.Sleep(2 * time.Second)
ch <- 2
}
func worker3(ch chan int) {
time.Sleep(3 * time.Second)
ch <- 3
}
The result is that worker #1 finished the job.
Great materials to read: