Kotan Code 枯淡コード

In search of simple, elegant code

Menu Close

Concurrency in Go with Channels and Goroutines

So far I’ve been walking myself through learning some of the basic features of the Go language and I’ve been impressed with what I’ve seen. I still have this nagging feeling that Go is a solution looking for a problem and I haven’t decided if that’s because I haven’t yet had that “a-ha!” moment where I figure out what problem Go solves miraculously or if it’s just because I’ve only been poking around with it for a few days.

Concurrency is one of the more heavily lauded features of the language, but the community is quick to remind you that concurrency is not parallelism. I won’t go into detail on that subject because it’s a really meaty one and I just want to provide a quick introduction to concurrency here.

The way to think about goroutines is that they are very small, light-weight chunks of processing that you compose in order to chew up a larger task. There may be a correlation here between goroutines and actors and in terms of design, I’ve noticed a near 1:1 correlation between what I would normally code into an actor with what I find being coded into goroutines in samples. The way goroutines communicate with each other and the rest of your code is through channels which can send and receive messages.

One distinction that I think is unique to Go is that sending and receiving on channels is a synchronizing operation. In other words, if you use the arrow notation (<-) to receive a value from a channel, you will block until you receive that value. Conversely, if you send a value on a channel, the code that sent the value will be blocked until some other goroutine plucks that value off the channel .You can get around this with buffered channels, but it seems to be a fundamental thing that goroutines synchronize via communication rather than shared memory.

In my little sample, I decided to take the synchronization “hello world” of a producer and a consumer and implement it in Go. In the following code, I’ve got a type called StockTick and I’ve created a channel that carries stock ticks (channels are strongly typed). I invoke a producer and a consumer and thus have two separate blocks of code communicating with each other:

package main

import (
	"fmt"
	"math/rand"
)

type StockTick struct {
	Ask		float32
	Bid		float32
	Symbol	string
}

func genTick() (tick StockTick) {
	tick.Ask = 45 * rand.Float32()
	tick.Bid = 42.50 * rand.Float32()
	tick.Symbol = "IBM"
	return tick
}

func produceTicks(stocks chan StockTick) {
	for x:=1; x<100; x++ {
		stocks <- genTick()
	}
	close(stocks)
}

func consumeTicks(stocks chan StockTick) {
	for stock := range stocks {
		fmt.Printf("Consumed a tick %v\n", stock)
	}
}

func main() {
	fmt.Printf("Stock Channels FTW\n\n")
	stocks := make(chan StockTick)
	go produceTicks(stocks)
	consumeTicks(stocks)
}

There are a couple of things that I found interesting in this code that caught me by surprise. The first is that I had to close the stocks channel when I was done producing. This is because the range operator creates a deadlock if the code reaches a point where it knows that there can be no more values to consume (the producer goroutine is done). You can get around this in real-world apps where you plan on consuming all day with an infinite loop, but in this case I knew I was done so calling close(stocks) is actually what allows the range operator to terminate in a friendly way without a deadlock.

Next, notice that I used the go keyword to fire off the produceTicks method as an asynchronous goroutine but I didn’t do that for consumeTicks. This is because if I let them both be asynchronous in the background, the main() function would terminate, which kills the go application. In a go app, as soon as main() exits, the entire app quits, even if you have active goroutines.

There are a number of higher-level patterns that you can implement with channels such as sync’ing on acknowledgements sent back on the channel from which a value is received and using a “fan-in” approach where you’re pulling from multiple channels to produce values on a single channel so that you aren’t waiting in lock-step for values to arrive in sequence across channels. As this is still just a series of blog posts covering my introduction to the language, I won’t cover those yet.

As I said, I am still unsure whether I have any problems that Go would be ideal to solve, but I am going to keep plugging away until I decide one way or the other.