Go inherently supports concurrency through the use of Goroutines and Channels. These mechanisms are completely managed by the Go runtime making them a better choice over the use of threads
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.
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.
Join the conversation