loading...
Cover image for Get a Taste of Concurrency in Go

Get a Taste of Concurrency in Go

dpkahuja profile image Deepak Ahuja πŸ‘¨β€πŸ’» Updated on ・7 min read

In 2006 Intel released the first dual core CPU. This was after that a language (Go) that could natively provide features to benefit from multi-cores came into existence.

Introduction

Concurrency is executing a set of instructions in any indefinite order and yet be able to produce same output of program as if functions are executed sequentially. Simultaneous execution of instructions is seen everywhere in modern day programs. For example: a web server serving multiple web requests or compiling code in our IDE while still being able to edit it. This term is often confused with Parallelism.
Say, If you have a sandwich a milk shake to finish in 3 minutes. You can do two things:

Choice 1:

  • Either take a bite of sandwich, chew.
  • Take a sip of shake, drink.
  • Repeat the process until you finish both under 3 minutes.

Choice 2:

  • You can take put the whole sandwich in your mouth, Bottoms up the milk shake.
  • Try swallowing both of them together.
  • Wait till you finish both under 3 minutes.
    My eating super powers

Choice 1 is Concurrency, Choice 2 is Parallelism. You can chose any depending upon the time, place and vicinity of friends to take full benefit of each. πŸ˜›

Two functions may be able to run concurrently but they may not be Parallel. Parallel execution means two or more independent instructions are being evaluated at same time, this is only possible if there are multiple cores available to run those instructions. Concurrency is about dealing with lot of things at once. Parallelism means doing those things at one time.

Visual-1

Parallelism and Concurrency

To understand Concurrency, we will first see a program that runs it's set of instructions in a sequence. Let's consider a simple program that checks whether a site is up or not by making a call to it's homepage.

A Slice of strings which are looped while making http GET request to each link
  1. On line:9, we declared a slice of strings (say array)
  2. On line:16, we declared a range to loop over the slice of strings and call checkLink function for each element (that is link) in slice.
  3. On line:22, The checkLink function makes a GET request to the link, wait for the response to come back and then logs the success or error response.

Output:


As observed in output, there is a distinct delay after each link is called. For each request we wait for request to come back with response. In between each fetch there is no other path of execution that can be executed. This makes it a sequential program.

Visual-2

Flow of the program

The path of execution of a code is called a routine. In Go they are called goroutines. Each Go program comes with minimum one goroutine which is the main function from which program execution begins.

The time slice of waiting has blocked our routine to proceed further, It is a wastage of multi core resource we have. Let's use goroutine to solve this.

Goroutine

A goroutine in Go gives our program a new path of execution. These can be termed analogous of Threads in languages like C# and Java (They are different but let's draw some picture here). But goroutine has a lighter footprint on the system. A thread consumes 1MB of memory due to the bigger stacksize, a go routines starts with only a fraction of that (2KB) as it's stack is resizable.

Think of goroutines as application level threads, Just as OS Threads are switched on and off the hardware cores by Operating System, goroutines are also context switched on each assigned OS Thread.

You can get number of CPU cores available for your program using:

    // NumCPU returns the number of logical
    // CPUs usable by the current process.
    fmt.Println(runtime.NumCPU()) 

Goroutines have no unique identifier, name, or data structure that they return, they are only anonymous workers. If interested seasoned gophers can read about it here.

Using Goroutine

A new routine can be started by using keyword go followed by a function call, the arguments are evaluated in main go routine but the function is executed in a new go routine.

    for _, link := range links {
         go checkLink(link)
    }

Now each checkLink function executes in a new goroutine (path of execution) and main goroutine is not blocked.

Output:


As you can see there is nothing is logged on console. The main goroutine fired new goroutines for each link, but it did not wait for them to be finished.
As there is nothing to execute after the loop which fired goroutines, the program exited.

Visual-3

Multiple goroutines running simultaneously

It will be evident by following code:

Output

Spawning more goroutines may not necessarily result in higher performance of your program as they may not execute at same time. We will learn about it at end of this article.

We need a way to communicate among different goroutines (here, to inform the main goroutine about their execution) and synchronise their tasks. To make a goroutine publish a message to another goroutine we need a PIPE. On one end, the goroutine can publish the message, and on other end another goroutine which needs can listen using what we know as Channels.

Channels

Channels are these pipes via which different routines can send and receive values of a certain type between different goroutines.

Visual-4

Multiple goroutines communicating with the help of channels

Quoting from official golang tour:

Channels are a typed conduit through which you can send and receive values with the channel operator, <-.

To declare a channel we use:

    c := make(chan int)

    go func() {
        c <- 42 // Send data to channel
    }()

    fmt.Println(<-c) // Receive data from channel

Play it here

  1. In the channel 'c', we can send and receive integer values. Note: The data flows in the direction of arrow
  2. Once a routine sends something to the channel, the code execution routine is blocked (by default) until the value sent to the channel is being received on the other end at the same time.
  3. By same time, we mean the routine in which the channel is expecting to receive should be ready to receive at same time when the data is sent on channel. see here, we cannot send and receive in same goroutine because to send to a channel the receiving end will become active at a different time that is when code execution reaches that statement of receiving channel (<-c).
  4. If we there is no receiver, the routine will will block forever and eventually will lead to fatal error in go. You can try it here

Let's apply goroutines and channels to our program.

Output

Note: The output will vary as per internet speed and number of GOMAXPROCS that is number of goroutines that can execute at once.

We can use a familiar syntax of traditional for loop to receive from the channel or use a range loop from Golang.

    for index := 0; index < len(links); index++ {
        fmt.Println(<-c)
    }

The traditional loop is blocking because it cannot complete the iteration until it receives something from any of the channels.
The range loop from Go on the other hand, receives values from the channel repeatedly until it is closed.

    for msg := range c {
        fmt.Println(msg)
    }

Note: To come out of range loop we should always close from the sending channel otherwise it would keep waiting forever.
Here's the final code:

Spawning more goroutines than actual number of OS threads may slow down the program. For some situations writing concurrent program becomes obviously beneficial. Remember your sandwich and milkshake? It may be a good idea to follow Case 2 when you have bet on your eating superpowers but may not be when you are on your first date with your crush. πŸ˜›

To Summarise

By Understanding the type of work your program is going to handle, we can make a wise decision.
For CPU heavy work (like an awfully large for loop or calculating fibonacci of a rather big number) the threads are never idle. In this case parallel running goroutines on multiple OS/hardware threads are going to produce performance boost. If you have more goroutines than available OS/hardware threads, there is going to be a latency cost by switching goroutines on and off on each thread. More routines would have to wait.
For IO heavy work (like accessing file system or making network calls) the threads have to wait for OS to complete the task. In this case the OS/hardware will become idle and other goroutines will get a chance of execution. Hence concurrency without parallel execution will benefit here.
winner

You did great πŸ˜‡

Thank you for making it this far.
Important things like type of communication mechanism between channels, some design patterns around it and real world example left for a separate discussion. You can shoot your doubts or drop a hello on my twitter. Please consider some feedback or sharing the article for those who may benefit from it.

Posted on by:

dpkahuja profile

Deepak Ahuja πŸ‘¨β€πŸ’»

@dpkahuja

Lifelong learner, Software Engineer. Not in Forbes 30 under 30.

Discussion

pic
Editor guide
 

Nice article. The last example doesn't exist and if I understood well:
"To come out of range loop we should always close from the sending channel otherwise it would keep waiting forever" is were the problem is.

 

The output of last code listing is same as that of before, ithe dev.to platform is unable to render the output, some service worker problem. The gif link and all is fine though.

"To come out of range loop we should always close from the sending channel otherwise it would keep waiting forever"

The for loop knows how many receiving it will get as it has a terminating condition, but range loop does not.
The blocking will happen in case you use a range loop. And in this example the child goroutine doesn't know when to close the channel. You would observe in output that program doesn't exits after printing response of all 5 web requests.

 

Thank you for the great explanation! Next time please make gifs faster, they play so slow.

 

Thanks for reading! Great feedback. πŸ˜€

 

Wow! Amazing article so far. Best explanation with diagrams which makes it more easy to understand. Keep it up. Please make an article on 'why to choose go lang'

 

Thanks for the feedback. Sure i will share my experience on learning golang why i chose it as my next language.