This post is part of a series. For a listing of all the posts, as well as instructions on running the code, see here.
Turns out that despite all the shilly-shallying in the previous posts, we didn’t talk about one of Go’s most useful features, built-in concurrency implemented using go-routines and channels. Today seems like as good a day as any to give it a shot.
Concurrency in Go is really pretty simple, when invoking a function we want to run in parallel to the execution flow, we just use the go
keyword before the method invocation:
speak() // call the speak function and return when done
go speak() // create a go-routine in which speak run, returning immediately
To communicate between go-routines, channels are used. A channel is a typed conduit, which is most easily understood by means of an example:
var ch = make(chan string)
func speak() {
s := <-ch
fmt.Println(s)
}
func main() {
go speak()
ch<-"hi"
}
First off, we create a channel ch
with the type string. The speak function waits for input from this channel, and prints anything it receives before returning.
In the main function, we create a go-routine with go speak
. Once this is done, our speak
function is executing in parallel with our main
function, until it reaches the point where it encounters <-ch
. It then pauses waiting for input on the channel. The main
function meanwhile gets around to sending something down the channel ch
. When this happens, the speak
function continues executing, jovially printing out “hi”.
I’ll be honest, it was a bit of a challenge to think of nice clean example of how to use concurrency to generate an image. So instead, here is something a little more convoluted: imagine a network of nodes (e.g. towns on a map) where each nodes is connected to the 5 nearest nodes. This might look something like this:
I won’t go into the full details of how this is set up, as it’ll distract from talking about concurrency. The full nodes.go source is on github if you’re interested and want to play around with the parameters.
The important part is the Node object, which is defined as follows:
type Node struct {
Position Vector
Ch chan *Node
Peers []*Node
Canvas *Canvas
Power uint8
}
So, each Node
has a position, a channel to receive messages from other nodes on, a set of nearby peers and a power value.
To demonstrate concurrency, what we’ll do is hook up each Node, so that when it receives a message from another node, it will draw a line onto the canvas to visualize this. It will then send a message onto another node (that has yet to receive the message). The power is simply a way of keeping track of which nodes have been visited and showing how far a message has travelled.
Here’s an example of such a message propagating through a 50 node network:
The message starts off in yellow, and changes to red as it loses power with every step.
Of course it would be possible to write this in a non-concurrent way, but that would be no fun. In our setup, no Node has a full understanding of the network as a whole, and only knows how to blindly send on messages to its peers. However, put together this suffices for the message to traverse the network. Here’s the relevant code:
func (n *Node) Listen() {
// Listen for incoming connection on node's channel
for {
peer := <-n.Ch
peer.Power -= 5
n.Power = peer.Power
n.Canvas.DrawLine(color.RGBA{255, n.Power, 0, 255}, n.Position, peer.Position)
// Retransmit to random node
if n.Power > 0 {
go n.Send()
}
}
}
func (n *Node) Send() {
for _, target := range n.Peers {
if target.Power == 0 {
target.Ch <- n
break
}
}
}
Once you get your head around channels, there really isn’t much here. The Listen
method just sits there, waiting for an incoming connection on n.Ch
. Once it receives it, it decrements the Power
, draws the line and sends a new message using Send()
. Send
is even simpler, it picks a peer that has yet to receive a message and sends a message to its channel Ch
.
With all our nodes primed and ready for signals all that remains is to power up one Node
and instruct it to send a message.
node.Power = 255
go node.Send()
Now we can have some fun fiddling with the parameters and seeing what results we get. One thing that is pretty neat is that we can concurrently kick off multiple signals from different nodes and have them propagate the network at the same time.
Here’s the same 50 node network as before, but with 10 messages propagating, note how some messages get cut off by others and are forced to die early.
Or why not have 2500 nodes and see what happens?
Or if that seems like too little, how about 25000?
For the 25000 example above, the simulation ran no slower than when there were 100 nodes (although the initial network calculation is slow there). Go-routines are by design very light-weight, and this makes Go a great choice for writing concurrent code, e.g. for web servers.
As always, be sure to check out the full code on github