[Golang] Goroutine

2021-12-08 hit count image

Let's see how to use thread in Golang by Goroutine.

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.

  1. We won’t get any performance gains from concurrent programming. Even excessive locking can degrade performance.
  2. 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

You can use the applications that are created by this blog writer Deku. 지금 보고 계신 블로그를 작성하는 Deku가 개발한 앱을 한번 사용해보세요.
Deku created the applications with Flutter.

If you have interested, please try to download them for free.

Posts