20. Concurrency
Rob Pike - Concurrency is not parallelism1:
When people hear the word concurrency they often think of parallelism, a related but quite distinct concept. In programming, concurrency is the composition of independently executing processes, while parallelism is the simultaneous execution of (possibly related) computations. Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.
Go allows us to write concurrent programs. It provides goroutines and importantly, the ability to communicate between them.
20.1 Goroutines
They’re called goroutines2 because the existing terms—threads, coroutines, processes, and so on—convey inaccurate connotations. A goroutine has a simple model: it is a function executing concurrently with other goroutines in the same address space. It is lightweight, costing little more than the allocation of stack space. And the stacks start small, so they are cheap, and grow by allocating (and freeing) heap storage as required.
Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.
Prefix a function or method call with the go keyword to run the call in a new goroutine. When the call completes, the goroutine exits, silently.
go list.Sort() // run list.Sort concurrently; don't wait for it.
An example:
1 package main
2
3 import (
4 "fmt"
5 "math/rand"
6 "time"
7 )
8
9 func f(goRtn int) {
10 for i := 0; i < 3; i++ {
11 fmt.Println("Goroutine ", goRtn, " : Value ", i)
12 amt := time.Duration(rand.Intn(250))
13 time.Sleep(time.Millisecond * amt)
14 }
15 }
16 func main() {
17 for i := 0; i < 3; i++ {
18 go f(i)
19 }
20 var input string
21 fmt.Scanln(&input)
22 }
This program consists of two goroutines. The first goroutine is implicit and is the main function itself. The second goroutine is created when we call go f(0). Normally when we invoke a function our program will execute all the statements in a function and then return to the next line following the invocation. With a goroutine we return immediately to the next line and don’t wait for the function to complete. This is why the call to the Scanln function has been included; without it the program would exit before being given the opportunity to print all the numbers.
Goroutines are lightweight and we can easily create thousands of them. Our program runs 3 goroutines.
We add some delay to the function using time.Sleep3 and rand.Intn4.
f prints out the numbers from 0 to 3, waiting between 0 and 250 ms after each one. The goroutines should now run simultaneously.
The output I get is:
Goroutine 0 : Value 0
Goroutine 1 : Value 0
Goroutine 2 : Value 0
Goroutine 0 : Value 1
Goroutine 2 : Value 1
Goroutine 1 : Value 1
Goroutine 0 : Value 2
Goroutine 2 : Value 2
Goroutine 1 : Value 2
20.2 Channels
Channels provide a way for two goroutines to communicate with one another and synchronize their execution.
Channels have several characteristics: the type of element you can send through a channel, capacity (or buffer size) and direction of communication specified by a <- operator. Thus, you can send values into channels from one goroutine and receive those values into another goroutine with the channel operator, <-.
Channels are first-class values and can be used anywhere like other values: as struct elements, function arguments, function returning values and even like a type for another channel.
1 chan <- v // Send v to channel chan.
2 v := <-chan // Receive from chan, and
3 // assign value to v.
(The data flows in the direction of the arrow.)
Channels must be created using the built-in function make before use:
1 ch := make(chan int)
By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.
Let us look at a simple example:
1 package main
2
3 import "fmt"
4
5 func main() {
6 //Create a new channel with
7 //make(chan val-type). Channels are
8 //typed by the values they convey
9 messages := make(chan string)
10
11 //Send a value into a channel using
12 //the channel <- syntax. Here we send
13 //"ping" to the messages channel we
14 //made above, from a new goroutine
15 go func() { messages <- "ping" }()
16
17 //The <-channel syntax receives a
18 //value from the channel. Here we'll
19 //receive the "ping" message we sent
20 //above and print it out
21 msg := <-messages
22 fmt.Println(msg)
23 }
When we run the program the “ping” message is successfully passed from one goroutine to another via our channel.
By default sends and receives block until both the sender and receiver are ready. This property allowed us to wait at the end of our program for the “ping” message without having to use any other synchronization.
20.2.1 Channel direction
We can specify a direction on a channel type thus restricting it to either sending or receiving. For example if we have the following function signature:
1 func pinger(c chan<- string)
Now c can only be sent to. Attempting to receive from c will result in a compiler error. Similarly we can change signature to this:
1 func printer(c <-chan string)
A channel that doesn’t have these restrictions is known as bi-directional. A bi-directional channel can be passed to a function that takes send-only or receive-only channels, but the reverse is not true.
20.2.2 Unbuffered channel
Let’s understand this with an example:
1 package main
2
3 import "fmt"
4
5 func main() {
6 fmt .Println("From main()")
7
8 // Create a channel to synchronize goroutines
9 done := make(chan bool)
10
11 // Execute fmt.Println in goroutine
12 go func() {
13 sum := 0
14 for i := 0; i < 10000; i++ {
15 sum += i
16 }
17 fmt.Println("From goroutine - done.")
18
19 // Tell the main function everything is done.
20 // This channel is visible inside this goroutine because
21 // it is executed in the same address space.
22 done <- true
23 }()
24
25 <-done // Read operation: Wait for the goroutine to finish
26 }
Observe the output. The line “From main()” is blocked i.e. it is not printed until the goroutine has writen data to the channel. Thus the output you see is:
From main()
From goroutine - done.
both being printed at the same time.
Note: If the channel is unbuffered, the sender blocks until the receiver has received the value.
In the above program, the done channel has no buffer (as we did not specify its capacity). All operations on unbuffered channels block the execution until both sender and receiver are ready to communicate. That’s why unbuffered channels are also called synchronous. In our case the reading operation <-done in the main function will block its execution until the goroutine will write data to the channel. Thus the program ends only after the reading operation (<-done) succeeds.