I have continued my journey into the Go programming language and want continue writing about the cool stuff that Go does. This is partly to ensure that I understand it fully and to impart knowledge. In fact, one of the best ways to learn is to teach, so here goes.
Concurrency vs. Parallelism
The first order of business is to define what concurrency is and is not. There is a reason why I didn’t mention programming threads when talking about what other languages offer. This is because you don’t need threads to do concurrent programming, although you can use them if necessary. In fact, the async and await keywords in C# (and the Tasks that back them up) do not always spawn threads. Many times it is not necessary. This is because concurrency and parallelism are not the same thing.
Rob Pike on the golang.org blog (here) gives a succinct comparison of the two concepts:
Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.
Your work day illustrates this. When you get to work, you likely have emails waiting for you. These emails likely will spawn different tasks for you to do, and you might have had multiple tasks to do before that. However, you are only one person (or thread) and you have to deal with all of these tasks. For me, this means that when I have free time, I take on the first task and make as much progress as I can. If I can finish, that’s great; if not, I’ll switch to another task and make more progress on that second task. At no point do you do these tasks simultaneously, but you do end up getting multiple things done over a period of time.
The reason this is important is because of the construct that Go uses to handle concurrency in its programs. These are called go routines. The awesome bit about go routines is that they offer an abstraction layer over processor threads. This means that not every go routine requires a new thread to be created. Go routines take up about 2KB of memory, compared to the 1MB needed for the average thread. This allows Go to greatly reduce the memory footprint of its programs and greatly increase the efficiency. The other great thing is that the developer doesn’t have to think about the complexity involved with handling threads.
Just sticking the go keyword in front of a function will turn it into a go routine. This can especially be useful when spawning anonymous functions to go do something in the background. For example, the below code creates an anonymous go routine that reaches out to a web service and grabs the response:
This code is pretty straightforward, but it illustrates how easy it is to use go routines. Go handles the dirty work of whether a thread is needed or not, but we still get the benefit of not blocking until the web service returns a response to us. We are free to move to another task in the meantime.
If your program really calls for true parallelism, you can use the GOMAXPROCS function call (available from the “runtime” package) to tell Go to use multiple processors. Simply call the function and pass in the number of processors you want to use. Then Go will make use of the processors available to provide true parallelism. Be careful though, as this is not a silver bullet and may actually slow your program down if not used wisely. Here is what it looks like:
One of the biggest challenges once you try to do things concurrently or in parallel is side effects. If two functions executing at the same time try to change the same data, a race condition occurs. The last one to change the value will win. Go beautifully handles this using channels. Channels provide a way for go routines to pass data between themselves. The syntax, as usual with Go, is simple and self explanatory:
This little piece of code is a hello world program with channels. You use the make function to create a channel, tell it what type to expect (a string in this case), and how many it can hold at once (one in this case). The second line of the main function puts “Hello” into the channel. The Println() call in turn pulls the “Hello” out of the channel and displays it. So go routines can put data onto a channel and then another go routine can pull it off when it is needed (as long as it is a buffered channel).
There is one great thing that Go does that removes the threat of race conditions: Once data is put into a channel by a function, all references to that data in the function are made invalid. This ensures that only one go routine is able to change the data at one time. Brilliant!
You can even use a for loop with a range to loop over the contents of a channel:
This example creates a channel, loops over a slice (think array) of strings (the words variable), and then puts each string it finds into the channel. The close call closes the channel, which doesn’t allow any other data to be added to it. Then a for loop is used with the range keyword to loop over the channel and pull every value out until there are no more and print them out.
The world is your channel
The world of concurrent programming in Go is certainly full of all sorts of great stuff. I know I’ve only scratched the surface and can’t wait to learn more and get better. I am very excited and impressed by the care taken by the creators of Go when it comes to concurrent and parallel programming in Go. I look forward to what lies ahead.
Stay safe. Code safe.
P.S. For a great exploration of this topic, check out Concurrent Programming with Go by Andrew Van Sickle on Pluralsight. I got some great insight from this course.