We can make a program concurrent if we can split it as multiple independent tasks executed at the same time.

In concurrency, we execute different portions of a program at the same time (on single or multiple CPU threads) whereas in parallelism a singular task (or subtasks) is executed parallelly on multiple CPU threads.

Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.

Rob Pike, Co-Creator of Go

Goroutines

By adding the go keyword before a statement we can start a new goroutine. It will create a lightweight execution thread to execute the statement concurrently with the succeeding tasks in its parent function.

The program will end once the main goroutine is finished irrespective of the current state of other goroutines.

// Example of a non-concurrent program

package main

import (
        "fmt"
        "time"
)

func testFunc() {
        for j:=0;j<3;j++{
                fmt.Println("Hello from testfunc")
        }
        time.Sleep(2*time.Second)
}

func main() {
        testFunc()
        for i:=0;i<3;i++{
                fmt.Println("Hello from main")
                time.Sleep(2*time.Second)
        }
}

// Output
// Hello from testfunc
// Hello from testfunc
// Hello from testfunc
// Hello from main
// Hello from main
// Hello from main

The output of this program will be three statements from testFunc() functions followed by three statements from the main() function.

But, if we add the go keyword before testFunc(), we can create a new goroutine that will execute the testFunc() function concurrently with the statements in main().

package main

import (
        "fmt"
        "time"
)

func testFunc() {
        for j:=0;j<3;j++{
                fmt.Println("Hello from testfunc")
        }
        time.Sleep(2*time.Second)
}

func main() {
        go testFunc()
        for i:=0;i<3;i++{
                fmt.Println("Hello from main")
                time.Sleep(2*time.Second)
        }
}

// Output
// Hello from main
// Hello from testfunc
// Hello from testfunc
// Hello from testfunc
// Hello from main
// Hello from main

Channels

To send and receive values between Goroutines we have channels.

Channels are declared using the make(chan <datatype>) function with the datatype of the transmitted value, for example, make(chan string) will create a channel that could be used to send and receive string values.

chan<- string denotes a send-only channel and <-chan string denotes a receive-only channel for string values.

To iterate over the values received on a channel we can define a for loop with range like in the program below.

package main

import (
        "fmt"
        "time"
        "math/rand"
)

// Sends random integer on randomVal channel every 2 seconds
func getRandomValue(randomVal chan<- int, numValues int){
        for i:=0;i<numValues;i++{
                value := rand.Int()
                fmt.Println("getRandomValue(): Sent Random Integer", value)

                // Send a value to randomVal channel
                randomVal <- value

                // 2 seconds interval to ensure that the receiver gets enough time
                time.Sleep(time.Second*2)
        }
        close(randomVal)
}

// Listens to channel randomVal and prints received values on the terminal
func receiveRandomValue(randomVal <-chan int){

        // Loops over values received in channel
        for value := range randomVal{
                fmt.Println("recieveRandomValue(): Received Random Integer", value)
        }
}

func main() {
        randomValueChannel := make(chan int)

        go getRandomValue(randomValueChannel, 5)

        go receiveRandomValue(randomValueChannel)

        // To ensure that the main goroutine doesn't end 
        // before the other two goroutines
        time.Sleep(time.Second*10)
}

// Output
// getRandomValue(): Sent Random Integer 7680245871766720879
// recieveRandomValue(): Received Random Integer 7680245871766720879
// getRandomValue(): Sent Random Integer 1813843778283051521
// recieveRandomValue(): Received Random Integer 1813843778283051521
// getRandomValue(): Sent Random Integer 3359384099851901129
// recieveRandomValue(): Received Random Integer 3359384099851901129
// getRandomValue(): Sent Random Integer 3707965584748576540
// recieveRandomValue(): Received Random Integer 3707965584748576540
// getRandomValue(): Sent Random Integer 282243786570954167
// recieveRandomValue(): Received Random Integer 282243786570954167

A Go program finishes execution whenever the main goroutine is completed ignoring the current progress of other goroutines. To prevent this we can use a channel to flag completion of a goroutine.

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func getRandomInt(doneFlag chan bool){
	fmt.Println("Goroutine for getRandomInt() started")
	fmt.Println("Some random integer: ", rand.Int())
	time.Sleep(time.Second*5)

	// Will send value to the channel
	// once the function is finished
	doneFlag<-true
}

func main(){
	done := make(chan bool)

	go getRandomInt(done)

	// Main Goroutine will wait to receive a value on this channel
	<-done

	fmt.Println("Main Goroutine has ended")
}

// Output
// Goroutine for getRandomInt() started
// Some random integer:  4571662314887367842
// Main Goroutine has ended

If we ran this program without <-done the main goroutine would’ve ended the program preemptively and its output would be

Main Goroutine has ended

Buffering

A channel won’t send values unless there is another channel ready to receive those values. But Buffered Channels allow a certain amount of values to be sent without a corresponding receiver for those values.

package main

import "fmt"

func main() {
	exampleBufferedChan := make(chan string, 3)

	// Sending values to the buffered channel
	exampleBufferedChan<-"c"
	exampleBufferedChan<-"b"
	exampleBufferedChan<-"a"

	// Receiving values from the buffered channel
	fmt.Println("Value 1 from exampleBufferedChan:",<-exampleBufferedChan)
	fmt.Println("Value 2 from exampleBufferedChan:",<-exampleBufferedChan)
	fmt.Println("Value 3 from exampleBufferedChan:",<-exampleBufferedChan)
}

// Output
// Value 1 from exampleBufferedChan: c
// Value 2 from exampleBufferedChan: b
// Value 3 from exampleBufferedChan: a

The values from the channel will be received in the same order as they were sent.

Select

The select keyword is used to wait for values on multiple channels simultaneously. Its syntax is similar to a switch.

select executes the first channel to receive value in case statements. If multiple channels are ready with values then one of them is selected at random.

package main

import (
	"fmt"
	"time"
	"math/rand"
)

func randomValueGenerator(randomValueChannel chan<- int){
	time.Sleep(time.Second*5)
	randomValueChannel <- rand.Int()
}

func main(){
	channel1 := make(chan int, 1)
	channel2 := make(chan int, 1)

	go randomValueGenerator(channel1)
	randomValueGenerator(channel2)

	// Wait on channel1 and channel2 to receive values	
	select{
		case message1 := <- channel1:
			fmt.Println("Received value in channel1:", message1)
		case message2 := <- channel2:
			fmt.Println("Received value in channel2:", message2)
	}
}

// Output
// Received value in channel2: 1923060859381349025

I recommend executing this program multiple times as channel1 and channel2 will receive values at the same time and the output will be chosen at random by select.

Timeouts using time.After()

The time.After() function returns a value after the time specified in its parameter. It could be used as a timeout case in select statements.

package main

import (
	"fmt"
	"time"
)

func sendOnChannel(doneFlag chan<- bool){
	// It will take at least 5 seconds for this function to finish
	time.Sleep(time.Second*5)
	doneFlag<-true
}

func main(){
	channel1 := make(chan bool)
	channel2 := make(chan bool)

	go sendOnChannel(channel1)
	go sendOnChannel(channel2)

	select{
	case <-channel1:
		fmt.Println("Channel 1 has responded")
	case <-channel2:
		fmt.Println("Channel 2 has responded")

	// Added a 2 seconds timeout in the select statement
	case <-time.After(time.Second*2):
		fmt.Println("You've hit a timeout")
	}
}

// Output
// You've hit a timeout

Non-Blocking Channel Operations with default

A default statement is defined in select to avoid blocking from unbuffered channels.

package main

import (
	"fmt"
)

func main(){
	channel1 := make(chan bool)

	select{
	case <-channel1:
		fmt.Println("Something received on channel1")

	// Since there is no value received on channel1
	// the default case will be executed
	default:
		fmt.Println("This is default statement")
		fmt.Println("No value was received on channel1")
	}
}

// Output
// This is default statement
// No value was received on channel1

Closing Channels

We can close() a channel to stop it from accepting further values.

package main

import (
	"fmt"
)

func main() {
	valueChannel := make(chan int)

	go func(){
		for i:=0;i<5;i++{
			fmt.Println("Sending",i,"to channel valueChannel")
			valueChannel <- i
		}
		
		// Closing valueChannel to stop it from receiving further values
		close(valueChannel)
	}()

	go func(){
		for{
			value, openFlag := <- valueChannel
			if !openFlag{
				fmt.Println("valueChannel is closed")
				break
			}
			fmt.Println("Received value:", value)
		}
	}()

	<-valueChannel
	fmt.Println("All values received")
	fmt.Println("Main Goroutine finished")
}

// Output
// Sending 0 to channel valueChannel
// Sending 1 to channel valueChannel
// Sending 2 to channel valueChannel
// Received value: 1
// Received value: 2
// Sending 3 to channel valueChannel
// Sending 4 to channel valueChannel
// Received value: 3
// Received value: 4
// valueChannel is closed
// All values received
// Main Goroutine finished

WaitGroups

By default a Go program is finished when the main Goroutine is completed, We can add statements that wait for receiving over a channel like <-done once all goroutines are finished.

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func fastRoutine(done chan bool)(int){
	// It will take at least 2 seconds to execute this function
	fmt.Println("Fast Routine started")
	time.Sleep(time.Second*2)
	done <- true
	return rand.Int()
}

func slowRoutine(done chan bool)(int){
	// It will take at least 5 seconds to execute this function
	fmt.Println("Slow Routine started")
	time.Sleep(time.Second*5)
	done <- true
	return rand.Int()
}

func main(){
	done := make(chan bool)
	done2 := make(chan bool)

	go func(){
		fastRandomVal := fastRoutine(done)
		fmt.Println("Value received from fast routine:", fastRandomVal)
	}()

	go func(){
		slowRandomVal := slowRoutine(done2)
		fmt.Println("Value received from slow routine:", slowRandomVal)
	}()

	<-done
	<-done2
}

// Output
// Slow Routine started
// Fast Routine started
// Value received from fast routine: 1556543771289280615
// Value received from slow routine: 2312691093026658070

But sync.WaitGroup provides a much more elegant way to monitor multiple Goroutines at once.

package main

import (
	"fmt"
	"sync"
	"math/rand"
	"time"
)

func fastRoutine()(int){
	fmt.Println("Fast Routine started")
	time.Sleep(time.Second*2)
	return rand.Int()
}

func slowRoutine()(int){
	fmt.Println("Slow Routine started")
	time.Sleep(time.Second*5)
	return rand.Int()
}

func main(){
	// Created a WaitGroup
	var wg sync.WaitGroup

	// Incrementing WaitGroup counter by 1
	wg.Add(1)
	go func(){
		// Decrement WaitGroup counter by 1 
		// after the goroutine is finished
		defer wg.Done()
		fastRandomVal := fastRoutine()
		fmt.Println("Value received from fast routine:", fastRandomVal)
	}()

	wg.Add(1)
	go func(){
		defer wg.Done()
		slowRandomVal := slowRoutine()
		fmt.Println("Value received from slow routine:", slowRandomVal)
	}()

	// Wait for the WaitGroup counter to reach 0
	wg.Wait()
}

// Output
// Slow Routine started
// Fast Routine started
// Value received from fast routine: 4966166931659874487
// Value received from slow routine: 439978995889306642

WaitGroup starts a counter at 0 and a delta (int value) is added to it using the Add() method. The Done() method decrements the counter by 1 and the Wait() method waits for the counter to reach 0 and moves to the next statements in the program.


Thank you for taking the time to read this blog post! If you found this content valuable and would like to stay updated with my latest posts consider subscribing to my RSS Feed.

Resources

Concurrency vs Parallelism
Concurrency is not Parallelism
[Slides] Concurrency is not Parallelism
channels
WaitGroup