Finally, we get to the reason we're using Go for this part of the course: concurrency.
A goroutine is a function call that is run concurrently with the code that calls it, and any other goroutines.
To create a goroutine, just make a function call with the go
keyword before it:
go doWork(x) doOtherWork()
A goroutine is a user thread: it's cheap to create and having thousands of them is okay.
The Go runtime environment takes care of creating goroutines, and running them in kernel threads to get parallelism. It will make sure there are enough
kernel threads. (i.e. Go uses hybrid threading.)
A real example:
func Count(ident string, n int) { for i := 1; i <= n; i++ { fmt.Printf("%s%d ", ident, i) time.Sleep(50 * time.Microsecond) } } func testGoroutines1() { go Count("a", 5) go Count("B", 4) fmt.Scanln() // wait so we can see all goroutines finish }
The goroutines are truly running concurrently, with all of the uncertainty that comes with that. I saw these outputs in three consecutive runs:
a1 B1 a2 B2 a3 a4 B3 B4 a5
a1 B1 a2 B2 a3 B3 B4 a4 a5
a1 B1 B2 a2 a3 B3 a4 a5 B4
It's also possible to have a goroutine run an anonymous function:
a := 3 go func(n int) { fmt.Println(n) }(a)
There's a lot of syntax there. The pieces:
func(…) { … }
: anonymous function definition.func(…) { … }(a)
: call the anonymous function.go func(…) { … }(a)
: run in a goroutine.An anonymous function is only going to be readable if it's very short and simple. Otherwise, I suggest giving it a name.
Anonymous functions are closures in Go. Variables in scope when it's defined are captured.
b := 5 go func(c int) { fmt.Println(b + c) }(6)
11
Goroutines are good at doing things (all of those just printed), but what if we want to return a value? Where does the return value go here?
func addBytes(a, b byte) byte { return a + b }
go addBytes(3, 4)
Nowhere (as far as I can tell). If we want values back from a goroutine, we need something else.
A channel is a (thread-safe) communications channel where you can send and recieve values of any type. Basics…
Create a channel with make
, giving the type it will send/receive. e.g. a channel to transmit int
values:
ch := make(chan int)
Send a message (of the correct type) to the channel with the <-
operator:
ch <- 383
And receive a message (in another thread) with the same operator on the other side of the channel name:
result := <-ch
A complete (but minimal) example. For a function to send/receive on a channel, we pass it in as an argument:
func addBytesChannel(a, b byte, result chan byte) { result <- a + b }
Call it by passing the channel, and then waiting for a result to arrive:
result := make(chan byte) go addBytesChannel(3, 4, result) res := <-result fmt.Println(res)
A less silly example: do some calculations on an array in parallel and then collect the results (in whatever order they arrive).
func countTransformed(values []float64) int { transformed := make(chan float64) for _, v := range values { go transformingCalculation(v, transformed) } count := 0 var tv float64 for range values { // iterate len(values) times tv = <-transformed if tv > 0 { count += 1 } } return count }
In that example, we assume that each goroutine will send exactly once to the channel, so there will be exactly len(values)
messages coming back on the channel.
The function assumes that it can receive that many times from the channel. If not, it will deadlock.
The results may come back in any order from the channel. In this case, the logic doesn't care, but in general it might be a problem.
That code creates one goroutine for each array element: if the array is big, that might be unreasonable. If the calculation is short, it might be wasteful.
We have seen range
to iterate over values in an array. It can also iterate over values from a channel.
But, it has to know when there are no more values to receive. A channel can be closed to indicate that no more values will be sent.
// Send up to n random values to the channel. func RandRand(n int, values chan float32) { nValues := rand.Intn(n) + 1 for i := 0; i < nValues; i++ { values <- rand.Float32() } close(values) }
Then a for
/range
will iterate through the values (and then exit the loop).
func PrintRands() { randValues := make(chan float32) go RandRand(5, randValues) for v := range randValues { fmt.Printf("%v ", v) } fmt.Println("done") }
Output will look like:
0.6109869 0.6371221 0.27911648 done
Closing the channel is necessary here to indicate that the for
…range
should exit. Otherwise, it's not necessary to close()
.
By default, channels block. That is, when code sends with ch<-
, it will wait at that line until some other goroutine does <-ch
to receive it.
Sometimes that's not okay: you might need to send a message and move on with other work, even if it isn't going to be received immediately.
e.g. here, all of the goroutines have to wait around until their message is received.
ch := make(chan int) for i := 0; i < 3; i++ { go func() { fmt.Println("about to send") ch <- 1 fmt.Println("done sending") }() } time.Sleep(100 * time.Millisecond) for i := 0; i < 3; i++ { fmt.Println("got message:", <-ch) }
We can create a buffered channel that has some capacity to hold messages until they are received.
In the example, we can change the channel creation and add a buffer length:
ch := make(chan int, 3)
The output without buffering (left) and with (right):
about to send about to send about to send got message: 1 got message: 1 got message: 1 done sending done sending done sending |
about to send done sending about to send done sending about to send done sending got message: 1 got message: 1 got message: 1 |
With the buffer, these goroutines can exit (or move on to other calculations).
What if we have more than one channel to worry about?
The select
statement lets you listen for any one of a collection of channels. The syntax is almost exactly like the switch
statement:
select { case x := <-channelX: fmt.Println(x) case y := <-channelY: fmt.Println(y) case z := <-channelZ: fmt.Println(z) }
A slightly more realistic example: we want to process values from two streams that are generated concurrently: [full code]
squares := make(chan int) roots := make(chan float64) go GenerateSquares(10, squares) go GenerateSquareRoots(10, roots) for i := 0; i < 20; i++ { select { case sq := <-squares: total += float64(sq) case rt := <-roots: total += rt } }
The idea: any channel that has a message to receive (buffered or not) can let the select
statement continue. We can deal with multiple streams of data as they arrive.
My plan for Go + concurrency: that is enough basics to see how the language deals with concurrency. There is a lot more to say about how to deal with the tools the language gives us.
I'm going to pause concurrency for now and come back later.