Concurrent Programming in Go

Concurrent Programming in Go

Play this article

Concurrency is the capability of dealing with lots of things at once. A good way to understand concurrency is by imagining multiple cars travelling on two lanes. Sometimes the cars overtake each other and sometimes stop and let others pass by. Another good example is your computer that runs multiple background tasks like messaging, downloading movies, running the operating system etc.

Parallelism is doing lots of things simultaneously and independently. It might sound similar to concurrency but it’s actually different. Let's understand it better with the same traffic example. In this case, cars travel on their own road without intersecting each other. Each task is isolated from all other tasks. Concurrent tasks can be executed in any given order. It is a non-deterministic way to achieve multiple things at once. True parallel events require multiple CPUs.

2.png

What is Goroutine?

A goroutine is an independent function that executes simultaneously in some separate lightweight threads managed by Go. GoLang provides it to support concurrency in Go.

package main

import (
    "fmt"
    "time"
)

func main() {
    go helloworld()
    time.Sleep(1 * time.Second)
    goodbye()
}

func helloworld() {
    fmt.Println("Hello World!")
}

func goodbye() {
    fmt.Println("Good Bye!")
}

In this example, first, the main goroutine started, then it invokes helloworld() function and helloworld goroutine started. After helloworld goroutine finishes its operation the main goroutine waits for 1 second and invokes the goodbye() function. If you omit the time function in main then, it will exit before the helloworld() finishes its execution. Let's understand the step here-

  1. main goroutine started
  2. Invokes helloworld and helloworld goroutine started.
  3. If there is no pause using sleep method, the main will then invoke goodbye() and exited before the helloworld goroutine finishes its execution.

Without time.Sleep()

$ go run HelloWorld.go 
Good Bye!

After adding time.Sleep(), helloworld goroutine is able to finish its execution before main exited.

$ go run HelloWorld.go 
Hello World!
Good Bye!

WaitGroups

WaitGroups are used to wait for multiple goroutines to finish. It blocks the execution of a function until its internal counter becomes 0.

Let's see a simple code snippet:

package main

import (
    "fmt"
)

func main() {
    go helloworld()
    go goodbye()
}

func helloworld() {
    fmt.Println("Hello World!")
}

func goodbye() {
    fmt.Println("Good Bye!")
}

Output

$ go run HelloWorld.go 

$

If we run the above program, it doesn't print anything. Because the main function got terminated as soon as those two goroutines started executing. So, we can use Sleep which pauses the execution of main function.

package main

import (
    "fmt"
    "time"
)

func main() {
    go helloworld()
    go goodbye()
    time.Sleep(2 * time.Second)
}

func helloworld() {
    fmt.Println("Hello World!")
}

func goodbye() {
    fmt.Println("Good Bye!")
}

Output

$ go run HelloWorld.go 
Good Bye!
Hello World!

Here, the main function was blocked for 2 seconds and all the goroutines were executed successfully. It might not create any problem for blocking the method for 2 seconds but at the production level, where each millisecond is vital, millions of concurrent requests can create a huge problem.

This problem can be solved by using sync.WaitGroup.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go helloworld(&wg)
    go goodbye(&wg)
    wg.Wait()
}

func helloworld(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Hello World!")
}

func goodbye(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Good Bye!")
}

Output

$ go run HelloWorld.go 
Good Bye!
Hello World!

The output is same as the previous one but it doesn't block the main for 2 seconds.

  1. wg.Add(int) : This method indicates the number of goroutines to wait. In the above code, I have provided 2 for 2 different goroutines. Hence internal counter of wait becomes 2.
  2. wg.Wait() : This method blocks the execution of code until internal counter becomes 0.
  3. wg.Done() : This will reduce the internal counter value by 1.

NOTE : If a WaitGroup is explicitly passed into functions, it should be added by a pointer.

Channels

In concurrent programming, Go provided channels that are used for bidirectional communication between goroutines. Bidirectional communication means either one goroutine will send a message and the other will read it. Sends and receives are blocking. Code execution will be stopped until the write and read are done successfully. Channels are one of the convenient ways to send and receive notifications.

Unbuffered channel: Unbuffered channels require both sender and receiver to be present to be successful operations. It requires a goroutine to read the data, otherwise, it will lead to deadlock. By default, channels are unbuffered.

Buffered channel: Buffered channels have the capacity to store values for future processing. The sender is not blocked until it becomes full and it doesn't necessarily need a reader to complete the synchronization with every operation. if a space in the array is available, the sender can send its value to the channel and complete its send operation immediately. After its execution, if a receiver comes, the channel will start sending values to the receiver and it will start its operation once it received values. As the sender and receiver are operating at different times, it is called asynchronous communication.

Syntax to declare a channel
ch := make(chan Type)
Declaration of channels based on directions
1. Bidirectional channel : chan T
2. Send only channel: chan <- T
3. Receive only channel: <- chan T

Writing and reading from a channel

package main

import (
    "fmt"
    "time"
)

func main() {
    msg := make(chan string)
    go greet(msg)
    time.Sleep(2 * time.Second)

    greeting := <-msg

    time.Sleep(2 * time.Second)
    fmt.Println("Greeting received")
    fmt.Println(greeting)
}

func greet(ch chan string) {
    fmt.Println("Greeter waiting to send greeting!")

    ch <- "Hello Rwitesh"

    fmt.Println("Greeter completed")
}
$ go run main.go 
Greeter waiting to send greeting!
Greeter completed
Greeting received
Hello Rwitesh

In the above code snipped, msg := make(chan string) is declaring a channel of type string. Then I passed the channel in goroutine greet. ch <-"Hello Rwitesh" allows us to write the message to ch.

The ch <-"Hello Rwitesh" blocks the execution of the goroutine as no one reads its value written in a channel. Hence in the main goroutine time.Sleep(2 * time.Second) terminates the execution without waiting for greet.

The second time.Sleep(2* time.Second) statement gives us the time to read from the channel. We read from channel using <-msg.

Closing channel: Closing channel indicates that no more values should be sent on it. We want to show that the work has bee completed and there is no need to keep a channel open.

package main

import (
    "fmt"
    "time"
)

func main() {
    msg := make(chan string)
    go greet(msg)

    time.Sleep(2 * time.Second)

    greeting := <-msg

    time.Sleep(2 * time.Second)
    fmt.Println("Greeting received")
    fmt.Println(greeting)

    _, ok := <-msg
    if ok {
        fmt.Println("Channel is open!")
    } else {
        fmt.Println("Channel is closed!")
    }
}

func greet(ch chan string) {
    fmt.Println("Greeter waiting to send greeting!")

    ch <- "Hello Rwitesh"
    close(ch)

    fmt.Println("Greeter completed")
}

Channel is closed by using close() like close(ch) on the above code snippet.

$ go run main.go 
Greeter waiting to send greeting!
Greeter completed
Greeting received
Hello Rwitesh
Channel is closed!