|A Programmer's Guide To Go Part 3 - Goroutines And Concurrency|
|Written by Mike James|
|Thursday, 09 January 2014|
Page 1 of 2
Go is renowned for its easy-to-use approach to concurrency - it is part of the language. In this final part of our look at the key points of Go we look in depth at how goroutines and channels work together to orchestrate concurrency and parallelism.
A Programmer's Guide To Go
Part 3 Concurrency, goroutines and channels
Go does take a particularly clean approach to object oriented programming but one of the reasons people are attracted to it is that it offers a very simple and efficient implementation of concurrency. While other languages offer threading and similar but lighter-weight ways of working with concurrency, Go hardly introduces anything new - you just use goroutines and channels for communication. However, you do need to be careful not to misunderstand what you are getting.
Go provides a very simply and direct approach - the goroutine.
A goroutine is just a function invoked using the go statement. Writing
the function can be a method and the parameters are evaluated as usual.
The big difference is that the calling program doesn't wait for the function to finish - i.e. this is a non-blocking call and the gofunction starts executing on its own, but in the same address space as the invoking code.
Any return values that a goroutine might have are ignored when it ends and if the invoking code is terminated the goroutine also terminates.
So if you want to start function doing something while the main program gets on with something else then you need a goroutine.
This all sounds very simple, but you need to be very clear how it all works so let's look at at the simplest possible example.
Let's start off with a function that just prints 1000 values
Notice that i is a local variable.
Now if we have a simple main function to call the function everything works as you would expect - there is nothing new here:
If you run this then you see the 1000 values printed and then the usual
"Success process exited with code 0"
message that indicated that the main function terminated without error.
Now convert the call to a goroutine
If you try this out you will discover that you don't see anything printed at all. The reason is that the main program comes to an end before the goroutine gets a chance to run - so no go.
To give the goroutine time to execute we need to keep the main function running. The most obvious thing to do is to put the main function into an infinite loop:
However you will discover that while the program doesn't end the goroutine still doesn't get a chance to execute.
What is going on?
Go makes use of a fairly simple scheduling algorithm. This may become more sophisticated in time, but at the moment its simplicity might surprise you.
By default only one thread is used to run goroutines.
There can be many goroutines allocated to the same thread. When you start a goroutine the main function keeps the thread and the goroutine is simply scheduled for execution. For the goroutine to actually run the main function has to block and release the thread. Putting the main function into an infinite loop doesn't cause it to be suspended; it simply keeps it busy.
Put simply, if you start any number of goroutines by default they all run on the same thread as the main program. Only when the main program blocks does one of the goroutines get a chance to run - the thread starts to execute one of them as soon as the main program blocks. Similarly if the executing goroutine blocks the thread runs another goroutine.
By default Go doesn't implement parallelism but instead it offers concurrency with a single thread switching its attention between different goroutines.
So how do we suspend the main function so that the goroutine gets a chance to run?
In the real world blocking operations generally involve I/O of some kind, be it to disk or via a network connection. You call a system function and it blocks the running thread while it waits for the data transfer to complete. Go provides a special data type - the channel - that is often used to implement a blocking exchange of data between goroutines, but more of this a little later.
For the sake of an example, the simplest way of blocking the main program or any goroutine is to use the time module and the Sleep method.
If you call Sleep(t) the thread running the function is freed up for t milliseconds. In that time it can run other goroutines. Each goroutine gets to run until it blocks and the thread is used to run another goroutine or perhaps the main function.
You may well recognize this description as being very similar to the way asynchronous languages work with a single thread of execution. By default this is how Go works but it can be modified to produce a true parallel execution as described later. Notice however that in Go's way of implementing concurrency there are no "callbacks". Instead each goroutine gets to run until it is blocked. Also notice that it is perfectly possible for a goroutine to hog the thread - this is cooperative multitasking. We have already seen an example of this where the main program entered an infinite loop and the goroutine never got a chance to run.
For the moment let's concentrate on understanding Go's default mode of operation.
To give the goroutine time to run we need to add a call to the Sleep method of the time object.
If you run this program you will see the goroutine print 0 to 999 and then the main program print "end". The goroutine didn't get a chance to run until the call to Sleep. Once it got the thread of execution the goroutine then ran until it completed and the thread returned to the main program to allow it to print "end".
Now consider the following program and try and work out what you expect it to print:
Notice that the only change it that now the goroutine has a call to Sleep in it.
What you see when you run the program is;
The reason should be obvious. The main program blocks for 1 millisecond and the goroutine gets to run. The goroutine prints 0 and then blocks by executing Sleep(1) the main program unblocks before the goroutine and hence it terminates and so does the goroutine.
You can play around with the time of the Sleep function to allow the goroutine to print more than just zero because it gets the thread back a few times.
Notice that none of these examples is to be taken as a suggestion that a real Go program that uses concurrency should perform such timing-based synchronisation. The purpose of the examples is to convince you that default Go concurrency is very simple and only uses a single thread.
It is also important to realize that goroutines all share the same memory and in particular the same globals. If a goroutine is invoked as a function literal then it not only shares the globals but because it is a closure it shares the local variables of the function it is defined in. This is a subtle point but worth remembering.
|Last Updated ( Friday, 10 January 2014 )|