Contents
Outline
In this blog post, I will show you how to use Goroutine in Golang. Also, I will talk about the thread briefly to understand the Goroutine. You can see the full source code of this blog post on the link below.
Thread
The thread is a flow of the execution in the program. Normally, the program has a single flow(thread), but in some cases, the program has multiple flows. This is called multi-threading.
CPU is just a calculator. It just calculates the values that are passed, so it doesn’t care about where the values come from and where they go. In multi-threading, the OS controls the threads, if the threads are more than the CPU, the OS makes the threads are switched to use the CPU. This is called the Context switching.
The context switching is when multiple threads run on a CPU, it makes the threads switch to use the CPU. If the switching occurs, the switching coast occurs, so the performance issue can occur.
Conversely, if the number of the CPU is the same as the number of the thread, the switching doesn’t occur, so there is no performance issue.
Goroutine
Goroutine is a lightweight thread in Golang. All programs executed by Golang run on the Goroutine. That is, the main function is also executed on the Goroutine.
In other words, every program in Golang must have a least one Goroutine.
In Golang, you can use the Goroutine to execute the function with the go
keyword like the below.
go FUNCTION()
To check this, create the main.go
file and modify it like the below.
package main
import (
"fmt"
"time"
)
func PrintAlphabet() {
alphabet := "abcdefghijklmnopqrstuvwxyz"
for _, v := range alphabet {
time.Sleep(200 * time.Millisecond)
fmt.Printf("%c", v)
}
}
func PrintNumbers() {
for i := 1; i <= 10; i++ {
time.Sleep(200 * time.Millisecond)
fmt.Printf("%d", i)
}
}
func main() {
PrintAlphabet()
fmt.Println("")
PrintNumbers()
fmt.Println("")
go PrintAlphabet()
go PrintNumbers()
time.Sleep(3 * time.Second)
}
When you execute the code, you can see the following result.
# go run main.go
abcdefghijklmnopqrstuvwxyz
12345678910
a12bc34d5ef67g8hi910jklmn
The PrintAlphabet
function prints the alphabet every 200
milliseconds and the PrintNumbers
function prints the 1~10
numbers every 200
milliseconds. In the normal function calls, you can see when a function is finished, the next function is executed.
When the go
keyword is used in here, you can see the functions are called out of order.
Lastly, you can see the code that waits 3
seconds in the main
function. The code makes the main Goroutine to wait for 3 seconds. If the code isn’t there, the main
function executes the PrintAlphabet
and PrintNumbers
functions on the other Goroutine, and then exits(The main Goroutine exits). So, the Goroutine(Sub Goroutine) that is executed by the main Goroutine also exits and no result is printed.
Wait sub Goroutine
As you see above, when the main Goroutine exits, the sub Goroutine exits together. If you know when the sub Goroutine is finished, you can wait it by using the Wait
function like above. However, we can’t know when the sub Goroutine is finished.
Golang provides the WaitGroup
feature to make the main Goroutine wait for the sub Goroutine.
var wg sync.WaitGroup
wg.Add(3)
wg.Done()
wg.Wait()
You can use the Add
function in the WaitGroup
to add the number of the sub Goroutine that the main Goroutine should wait for. After it, you can use the Done
function to notify the Goroutine is finished in the sub Goroutine, and the Wait
function to wait for all sub Goroutine in the main Goroutine.
To check this, modify the main.go
file like the below.
package main
import (
"fmt"
"sync"
"time"
)
func PrintAlphabet() {
alphabet := "abcdefghijklmnopqrstuvwxyz"
for _, v := range alphabet {
time.Sleep(200 * time.Millisecond)
fmt.Printf("%c", v)
}
wg.Done()
}
func PrintNumbers() {
for i := 1; i <= 10; i++ {
time.Sleep(200 * time.Millisecond)
fmt.Printf("%d", i)
}
wg.Done()
}
var wg sync.WaitGroup
func main() {
wg.Add(2)
go PrintAlphabet()
go PrintNumbers()
wg.Wait()
}
WHen you execute the code, you can see the result like the below.
# go run main.go
a1b23cd4e56fg78hi910jklmnopqrstuvwxyz%
Unlike the previous example, you can see the main Goroutine waits for all sub Goroutine to be done.
How Goroutine works
Goroutine is a lightweight thread that uses the OS thread. The Goroutine is not a thread, but it uses the thread.
Goroutine is executed on the OS thread. And if the number of Goroutine is more than the number of the OS thread, the Goroutine waits for the other Goroutine to finish. When the Goroutine that uses the OS thread is finished, the other Goroutine is executed.
Also, When the Goroutine uses the system call(Read, Write the files or network), the Goroutine doesn’t do anything before the OS response, so the Goroutine is put into the queue and the other Goroutine is executed.
If you make many Goroutine, the Goroutine are running on the OS thread, so the OS level context switching is not occurred.
Concurrency programming caveats
Programming using threads, such as Goroutine, is called Concurrency programming. At this time, if the same memory is accessed by multiple Goroutine(Threads), a concurrency problem will occur.
To check this, modify the main.go
file like the below.
package main
import (
"fmt"
"sync"
"time"
)
type Account struct {
Balance int
}
func DepositAndWithdraw(account *Account) {
fmt.Println("Balance:", account.Balance)
if account.Balance < 0 {
panic(fmt.Sprintf("Balance should not be negative value: %d", account.Balance))
}
account.Balance += 1000
time.Sleep(time.Millisecond)
account.Balance -= 1000
}
func main() {
var wg sync.WaitGroup
account := &Account{Balance: 10}
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
for {
DepositAndWithdraw(account)
}
}()
}
wg.Wait()
}
WHen the code is executed, the following result is printed.
# go run main.go
Balance: 6010
Balance: 10
Balance: 7010
Balance: 6010
Balance: 5010
panic: Balance should not be negative value: -1990
The code uses the Goroutine, so the result may not be the same. The DepositAndWithdraw
function access the Account
variable to increase and decrease 1000
. The example code has 10 Goroutine that executes the function infinitely.
If the function is executed in order instead of the Goroutine, the negative balance will never occur and the panic
won’t occur. However, multiple Goroutine changes the same memory value, so the panic
occurs by the concurrency problem. (If the panic doesn’t occur, exit the program and try to run again.)
Mutext
To solve this kind of concurrency problem, you can use the Mutex
(Mutual Exclusion) to lock
the memory for being accessed by only one Goroutine.
To check this, modify the main.go
file like the below.
package main
import (
"fmt"
"sync"
"time"
)
var mutex sync.Mutex
type Account struct {
Balance int
}
func DepositAndWithdraw(account *Account) {
mutex.Lock()
defer mutex.Unlock()
fmt.Println("Balance:", account.Balance)
if account.Balance < 0 {
panic(fmt.Sprintf("Balance should not be negative value: %d", account.Balance))
}
account.Balance += 1000
time.Sleep(time.Millisecond)
account.Balance -= 1000
}
func main() {
var wg sync.WaitGroup
account := &Account{Balance: 10}
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
for {
DepositAndWithdraw(account)
}
}()
}
wg.Wait()
}
When you execute the code, you can see the result like the below.
# go run main.go
Balance: 10
Balance: 10
Balance: 10
Balance: 10
Balance: 10
Balance: 10
Mutex can solve the concurrency problem simply but has the following problems.
- We won’t get any performance gains from concurrent programming. Even excessive locking can degrade performance.
- The Deadlock(make Goroutine stop completely) can occur possibility.
So, when you use the Mutex, you should be careful.
Deadlock
Let’s see the Dinig Philosophers Problem
that is one of the most famous concurrency problems to check the deadlock.
If you want to know more details about Dinig Philosophers Problem, see the Wiki
.
To check this, modify the main.go
file like the below.
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func diningProblem(name string, first, second *sync.Mutex, firstName, secondName string) {
for i := 0; i < 100; i++ {
fmt.Printf("(%s) Try to eat\n", name)
first.Lock()
fmt.Printf("(%s) Grab %s\n", name, firstName)
second.Lock()
fmt.Printf("(%s) Grab %s\n", name, secondName)
fmt.Printf("(%s) Eating\n", name)
time.Sleep(time.Duration(rand.Int()) * time.Millisecond)
second.Unlock()
first.Unlock()
}
wg.Done()
}
func main() {
rand.Seed(time.Now().UnixNano())
wg.Add(2)
fork := &sync.Mutex{}
spoon := &sync.Mutex{}
go diningProblem("A", fork, spoon, "Fork", "Spoon")
go diningProblem("B", spoon, fork, "Spoon", "Fork")
wg.Wait()
}
When the code is executed, you can see the result like the below.
# go run main.go
(B) Try to eat
(A) Try to eat
(A) Grab Fork
(B) Grab Spoon
fatal error: all goroutines are asleep - deadlock!
Mutex is a very simple solution to solve the concurrency problem. But it can make the deadlock and the program quit unintentionally.
Completed
Done! We’ve seen how to use Goroutine to use the thread in Golang. Also, we’ve seen how to wait the sub Goroutine to finish and the solution to solve the concurrency problem.
Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!
App promotion
Deku
.Deku
created the applications with Flutter.If you have interested, please try to download them for free.