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 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)
}
}
Output:
this prints out
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.
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()
}
Output:
this prints out
1
3
5
7
9
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)
}
Output (use go run --race main.go
):
Counter: 9
Found 1 data race(s)
exit status 66
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)
}
Output (use go run --race main.go
):
Counter: 10
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)
}
Output (use go run --race main.go
):
Counter: 10
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
- Great resource to learn more about mutex in Go
- Mutex in Go
- atomic package documentation
- Other example of using atomic package
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)
Amazing tutorial!
thanks 😄