Being Concurrent in Go

We mentioned Go inherently supports concurrency in our previous blog, didn't we?  And we'd said that we'll talk more on that later.Well now's the time!

Yes. Go does support concurrency through something called as goroutines and channels. But before going into that, let's brush up on a few concepts. what exactly does concurrency mean? How is it handled ? And what about parallelism then? Is it different?

Concurrency and Parallelism

I love creating food analogies (sorry about that!) and its pretty obvious that I am going to explain this concept using one.

Let's say you have to put together a typical fast food meal of burger and some fries. Now, you're the only one working and have to somehow ensure that you deliver the meal at the earliest (You don't want angry customers).

So you multitask. You know the patty takes 5 mins to cook and the fries take 3 mins to fry. So you put these two things first and while they are cooking, you start with assembling rest of the toppings for the burger. You'll be done in a little over 6 or 7 minutes. (Imagine if you'd done these things one after the other, it'd have a much longer time.) And that's how concurrency works!

Concurrency:
In Concurrency the application "seemingly" carries out more that one task at the same time (even though it may not be the case).

If there's a single CPU, then multiple tasks can be in progress but only one is executed at a time. The CPU keeps switching between the tasks during execution.

Now, You get another person to help you out. Things become even faster since you can now divide tasks among two people. Each person works independently and two things get done at the same time. Thats parallelism.

Parallelism:
In parallelism, smaller set of subtasks are created which are processed in parallel on multiple CPUs at the same time.

For true parallelism the application has multiple threads running where each thread runs on separate CPUs(or CPU cores)

Goroutines

In Go, concurrent tasks are carried out using goroutines.

In simple terms, a goroutine is just a function that will execute along with other functions concurrently.  Remember though, a goroutine is not the same as a OS thread. Instead, they run over OS threads.

Go routines have a bunch of benefits over threads. A thread operates at OS level whereas goroutines are completely managed by the Go runtime which means that they are hardware independent. Moreover, they are lightweight and have a very less startup time. But the biggest advantages of using goroutines are that they prevent any sort of deadlock or race conditions and the use of channels as a means of communication ( a feature which is not present when using threads).

So in the end, you can have millions of Goroutines running without being worried about the complexities.

In code, creating a goroutine isn't that fancy. A simple function call with a "go" prefix will be considered as a goroutine.

package main

import (  
    "fmt"
    "time"
)

func helloFromGoroutine() {  
    fmt.Println("Hello from Goroutine")
}
func main() {  
    go helloFromGoroutine()
    fmt.Println("Main func execution starterd......")
	/* adding small delay of 1 sec, so main func doens't exit before the execution of Goroutine */
    time.Sleep(1 * time.Second)
    fmt.Println("Main func execution completed......")
}
Sample Program for Goroutine
Main func execution starterd......
Hello from Goroutine
Main func execution completed......
Output of Sample Program
Using sleep in the main Goroutine is a hack we are using just to understand how Goroutines work. We use channels to block the main Goroutine until all other Goroutines finish execution.

Channels

Two Goroutines will communicate using a channel. Think of it as a telephone call between two people. One talks on one side while the other listens on the other side. Channels work the same way. A goroutine sends some information on a channel and another goroutine receives it.

Channels can either be unbuffered or buffered. To know the difference, let's extend our fast food analogy from earlier.

Unbuffered Channels

You are responsible for assembling the burgers and your coworker is the one packing them in paper bags. You will have to wait till your coworker can accept a new burger to pack (maybe he's not finished with the earlier ones). On the other hand, if your coworker is free, he will wait for you to pass a new burger to pack.

This is the case for unbuffered, the sender goroutine blocks on the channel till another accepts the data being sent on the channel. Conversely, if a receiver is waiting on a channel, it will block till a sender sends data across. Both the sender and the receiver "synchronise" on the channel and that's why unbuffered channels are called synchronous channels.

Buffered Channels

Now consider another case where there is a tray between the both of you which can hold three burgers at a time. In such a case, you drop the burgers in the tray and your coworker will pick them up from there. Nobody waits for each other. Of course, you'll have to wait if the tray is full (there are already three burgers in there) while your coworker will have to wait is the tray is empty.

This is the case for buffered channels. As their name suggests, buffered channels have a buffer of certain capacity with them, like a queue. The sender puts data at the back of the queue and the receiver picks it up from the front of the queue.

Channels in go are created with the "chan" key word. The arrow operator decides whether the operation is a send or receive.

// create a unbuffered channel to send integers
c := make(chan int)	
data:= 12
c <- data		// send operation
data = <-c		// receive operation

Channels are usually bidirectional but they can be forced to be unidirectional. This means a goroutine can only perform either a send or a receive operation on the channel.

c <- int		// send channel

<-c int			// receive channel

A simple implementation of goroutines and channels is:

package main

import (
	"fmt"
	"math/rand"
)

func generate(c chan int) {
	for i := 0; i < 10; i++ {
		x := rand.Intn(100)
		fmt.Println("Generated number ", x)
		c <- x
	}
	close(c)
}

func double(c2 chan int, c chan int) {
	for i := range c {
		c2 <- i * 2
	}
	close(c2)
}

func main() {
	c := make(chan int)
	c2 := make(chan int)
	go generate(c)
	go double(c2, c)
	for i := range c2 {
		fmt.Println("Doubled number: ", i)
	}

}

Summary

Goroutines and Channels together are an efficient method to implement concurrency in Go. These two features native to Go, make the language apt for developing concurrent systems that have to handle large number of workloads at the same time. We have only gone through a brief overview of concurrency only pertaining to Go but the concept is vast in itself. If you are interested, we are adding a few links below.

References:

Chapter 8. Goroutines and Channels - Shichao’s Notes
Concurrency — An Introduction to Programming in Go | Go Resources
Concurrency is not parallelism - The Go Blog
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.