Concurrency in Go
Marco Franssen /
9 min read • 1645 words
A reason to choose Go over other programming languages could be to have the need for building software which requires concurrency. Go is built with concurrency in mind. You can achieve concurrency in Go by using Go routines. A Go routine is a lightweidght thread to explain this in easy words for the people with c#
and Java
backgrounds. Please experienced Gophers don't take my words litterly as I do know a Go routine shouldn't be compared to threads like this, but at least it is the easiest for me to set the context.
In case this is the first time you work with Go I also wrote an blogpost on how to setup your development environment. Once you are settled you can continue the remainder of this blog post and learn about concurrency and parallelism in Go. We will also touch channels which allow us to communicate between Go routines. OH! There are again tons of examples included.
Go routines
As I already mentioned a Go routine has a lower footprint then threads, from programming languages like c# or Java. Where threads consume approximately arround 1MB of memory due to the bigger stacksize, a Go routine starts with only a fraction of that (2KB). With go routines we can achieve concurrency as well paralellism. To be clear on the difference for those 2 I would first like to elaborate a bit on this.
Concurrency != Parallelism
With concurrency we can achieve parallelism, but we can only achieve that if we have multiple processors cores. In general we can do 4 things in parallel if we have 4 processor cores. You will have an environment variable GOMAXPROCS
available to limit the amount of parallelism in your GO program, e.g. GOMAXPROCS=2
would only give paralelism on 2 threads even if your system might have 8. This allows you to test your code for example on machines with fewer cores or even disable parallelism by putting it on GOMAXPROCS=1
. Furthermore we could get the amount of available cpus via code using the runtime package.
Let's have a look at an example which runs on main and therefore not uses any concurrency, neither parallelism:
Above example keeps beeping every second until you quit the program. Try it out in the playground for yourself. Now lets try to run this on a go routine. For doing this we simply put go in front of our beep
function call.
Now run it again. You will notice nothing is printed as the program will immediately exit as we didn't wait for the go routine to finish. Lets proof with a poor man's example that if we have the main routine wait for 2 seconds that we will get 2 messages printed.
Using above example you will notice the beep runs for 2 seconds and then our program ends. So all go routines will exit as soon your program exits, so if you don't block or have your program wait in some manner, the go routine might just never execute as they are not guaranteed to run immediately. Now lets see how we can improve this by using channels to communicate between the 2 routines within our program.
Channels
A channel in Go is used to communicate between routines. You can compare it to a kind of queue where we have on one end of the queue a Go routine publishing messages and on the other side we have a Go routine consuming those messages. All things you normally would have to do with Thread syncing and stuff like this is handled by the go compiler and your channels. Lets see how we can define a channel which can receive messages of type string
We can write to the channel using following syntax beeps <- "Bleep"
. We can read from the channel using <-beeps
. Now lets take our original example and change this to communicate via a channel.
We changed the beep function to not print to console anymore but instead put the formatted string on the channel. The main function we changed to use a for loop which will consume 5 messages from the channel. Go will take care of the synchronisation so the program can write our message to the console. The syntax to write to a channel is pretty similar to assigning a value to a variable beeps <- "Awesomeness"
. To read from a channel we do <-beeps
which will basically pop the value form the queue. In above example we put the value we got in a String formatting function to print directly to the console. You could for example also assign the value to a variable like this lastBeep := <-beeps
.
Another thing I want to highlight is that we created an unbuffered channel. This means when we would run this code on a single CPU core the program would halt the go routine for a fraction of time to consume the value from the channel and print in. This is also known as context switching where the 2 go routines (main and sub routine) would do some synchronisation. You might not really notice it as you are probably running an a system with multiple cores, however you could limit that by setting GOMAXPROCS=1
trying to simulate the difference. Not sure if we can really see this as the code in previous example is not super CPU intensive.
Buffered channels
So imagine we would have more complex programs which would cause more context switching for longer then just a nano second and we don't really care about having things printed immediately. In that case we could create a buffered channel which buffers the given amount of bytes before it enforces the syncing / context switching. Note you will get some eventual consistency on the consuming side of your channel, but you enable the publishing routine to continue like crazy until the buffer is full. Again this is just a poor man's explanation, so bare with me and don't take my words literally.
Last but not least I would like to showcase an example which will wait until the program is exited by the user. For that we will use an additional channel and replace the for loop with a select statement to continuously check both channels.
Unfortunately above code will not run in the playground due to a limitation in the playground, so you will have to try this on your own machine. As you can see the second channel is a channel of the type os.Signal
which we buffer expliticly on one. After all we want to program to quit asap and we don't want to wait for any other signals to stop the program. Try out ctrl+c
which is one of the signals we want to get notified about on the quit channel. Another way is to kill the process using kill <process_id>
. This time I also used a buffered channel for the beeps as I don't mind I will see those beeps with small delay. I also applied a nice generator function which returns us a consume only channel and starts the beep at higher pace this time.
Try running this example locally and limit it to only run on a single CPU for example.
Due to the buffering the code might in some occasions not immediately consume from the channel as you run it only using one processor. So in general if oyu want Go routines to synchronise immediately you shall not buffer them. In cases where eventual conistency is fine (few milliseconds) you can buffer the channels which might give better performance on high load.
In following video Rob Pike will explain more about concurrency and parallelism in more detail. Furthermore I also recommend following presentation.
Another reference to learn more about Go routines is this tour. Thank you for your attention if you made it to this last sentence.