DEV Community

nadirbasalamah
nadirbasalamah

Posted on • Edited on

Golang tutorial - 9 Concurrency using goroutine

Concurrency and Parallelism

Concurrency is not a parallelism. Concurrency is a mechanism when two or more tasks can start, and run in overlapped time. Parallelism is a mechanism when many tasks can run at the same time. The illustration of concurrency and parallelism can be seen in the picture below.
Concurrency illustration

Concurrency in Go

There are two main components when creating a concurrent program in Go, there are goroutine and channel. goroutine is a lightweight thread that can be created using the go keyword.

Here is an example of creating goroutine.

func main() {
    fmt.Println("this prints out")
    //create a goroutine
    go odd()
}

//prints out odd numbers
func odd() {
    for i := 1; i < 10; i += 2 {
        fmt.Println(i)
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

this prints out
Enter fullscreen mode Exit fullscreen mode

Based on that code, notice that the fmt.Println("this prints out") is executed but the odd() function seems not executed. The odd() function is executed in another goroutine. The illustration of that code can be seen in this picture.
Goroutine illustration

Using WaitGroup

WaitGroup is a mechanism that can be used to synchronize the Go code. The basic usages in WaitGroup are:

  • Add() defines the number of goroutines that are involved.
  • Wait() defines the wait condition in certain goroutine.
  • Done() defines the finish condition in certain goroutine. It means that the operation inside the goroutine is finished.

An example of the usage of WaitGroup can be seen in this code:

package main

import (
    "fmt"
    "sync"
)

//initiate the waitgroup
var wg sync.WaitGroup

func main() {
    //add the 1 goroutine (in this case the odd() function is a goroutine)
    wg.Add(1)
    fmt.Println("this prints out")
    go odd()  //goroutine
    wg.Wait() //wait until odd() function is finished
}

//prints out odd numbers
func odd() {
    for i := 1; i < 10; i += 2 {
        fmt.Println(i)
    }
    //defines that the operation or job inside this function is finished
    wg.Done()
}
Enter fullscreen mode Exit fullscreen mode

Output:

this prints out
1
3
5
7
9
Enter fullscreen mode Exit fullscreen mode

Based on that code, the WaitGroup is used in the main() function to synchronize the code so the output from the odd() goroutine is visible.

Race Condition

The simple definition of race condition is a condition when write and read operations occur in the same variable at the same time.
Race condition makes the value of certain variables inconsistent and introduces bugs in a code.

In Go, checking the race condition in a code can be done by using the go run --race command.

Here is an example of race condition.

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var counter = 0

func main() {
    //declare a number of goroutines
    total := 10

    for i := 0; i < total; i++ {
        //launch a goroutine with anonymous function
        go func() {
            v := counter
            runtime.Gosched() //can be replaced with time.Sleep()
            v++
            counter = v
        }()
    }
    fmt.Println("Counter: ", counter)
}

Enter fullscreen mode Exit fullscreen mode

Output (use go run --race main.go):

Counter:  9
Found 1 data race(s)
exit status 66
Enter fullscreen mode Exit fullscreen mode

Based on that code, the race condition occurs when a variable called v assigns a value from the counter variable then the value of v increments by 1 and is assigned again into the counter variable. There are many solutions to solve race condition. The solutions are using mutex, atomic, and other alternatives that can be used.

Using Mutex

Mutex or Mutual Exclusion is a mechanism that can be used to solve a race condition in a code. Mutex is a mechanism that allows only one goroutine to run the critical section (a code with a potential race condition) to prevent race condition.

Here is an example of using mutex.

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var counter = 0
var wg sync.WaitGroup

//initiate a mutex
var mu sync.Mutex

func main() {
    //declare a number of goroutines
    total := 10

    wg.Add(total)

    for i := 0; i < total; i++ {
        //launch a goroutine with anonymous function
        go func() {
            //lock the operation to available for only one goroutine
            mu.Lock()
            v := counter
            runtime.Gosched()
            v++
            counter = v
            //unlock the operation to available for other next goroutines
            mu.Unlock()
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("Counter: ", counter)
}
Enter fullscreen mode Exit fullscreen mode

Output (use go run --race main.go):

Counter:  10
Enter fullscreen mode Exit fullscreen mode

Based on that code, the race condition can be solved by using a mutex. Basically, before entering the critical section the Lock() function is called to make sure only one goroutine is available to access the critical section. After entering the critical section, the Unlock() function is called to unlock the operation to make sure is available for the next other goroutines.

Using atomic

There is a package available in the Go programming language that can be used to solve race conditions. The package is called atomic. This package implements synchronization algorithms.

Here is an example of using atomic to solve race condition.

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

//initiate the counter variable that has a type of int64
var counter int64
var wg sync.WaitGroup

func main() {
    //declare a number of goroutines
    total := 10

    wg.Add(total)

    for i := 0; i < total; i++ {
        //launch a goroutine with anonymous function
        go func() {
            //using atomic package to increment a value in counter variable by 1
            atomic.AddInt64(&counter, 1)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("Counter: ", counter)
}
Enter fullscreen mode Exit fullscreen mode

Output (use go run --race main.go):

Counter:  10
Enter fullscreen mode Exit fullscreen mode

Based on that code, the atomic package can be used to increment a value in the counter variable safely. Notice that the AddInt64() function takes two arguments. The arguments are a pointer or memory address of a certain variable that the value needs to be added and a certain number to be added into a variable.

Notes

The concurrency mechanism using channels in Go will be covered in the next blog, I hope this article is helpful to learn the Go programming language. If you have any thoughts or feedback, you can write it in the discussion section below.

Top comments (2)

Collapse
 
delta456 profile image
Swastik Baranwal

Amazing tutorial!

Collapse
 
nadirbasalamah profile image
nadirbasalamah

thanks 😄