본문 바로가기
Go

[Go] goroutine and channel

by llHoYall 2021. 10. 23.

A goroutine is a lightweight thread in Go.

Goroutine

We usually use goroutine in those cases.

  • When one task can be split into different segments to perform better.
  • When making multiple requests to different API endpoints.
  • Any work that can utilize multi-core CPUs should be well optimized using goroutines
  • Running background operations in a program might be a use case for a goroutine.

We simply add a keyword go in front of the function for creating a goroutine.

There is one thing to keep in mind.

goroutine cannot have a return value.

func printA() {
  for i := 0; i < 10; i++ {
    fmt.Println("A", i)
  }
}

func printB() {
  for i := 0; i < 10; i++ {
    fmt.Println("B", i)
  }
}

func main() {
  go printA()
  go printB()
}

But, it will not work.

This is because when the main function is terminated, the goroutine is terminated immediately, even if it is running.

Therefore, it should be allowed to wait for the execution of the goroutine.

func printA() {
  for i := 0; i < 50; i++ {
    fmt.Println("A", i)
  }
}

func printB() {
  for i := 0; i < 50; i++ {
    fmt.Println("B", i)
  }
}

func main() {
  go printA()
  go printB()
  time.Sleep(10 * time.Millisecond)
}
// A 0
// A 1
// A 2
// ...
// B 47
// B 48
// B 49

It works now, but it is ungraceful.

Now, it's time to learn about the sync.WaitGroup.

package main

import (
  "fmt"
  "sync"
)

var wg sync.WaitGroup

func printA() {
  for i := 0; i < 50; i++ {
    fmt.Println("A", i)
  }
  wg.Done()
}

func printB() {
  for i := 0; i < 50; i++ {
    fmt.Println("B", i)
  }
  wg.Done()
}

func main() {
  wg.Add(2) // set number of jobs

  go printA()
  go printB()

  wg.Wait() // wait until all jobs are done
}

sync.WaitGroup helps goroutine.

It has three methods.

  • Add() method sets the number of jobs.
  • Done() method is called when a job is done.
  • Wait() method waits all set jobs are done.

The Pros of Goroutine

When we use goroutine, the CPU core and OS thread aren't changed. In other words, there is no context switching.

In goroutine, only goroutine itself changes.

Therefore, there are benefits in performance.

Precautions for Goroutine

A concurrency problem occurs when multiple goroutines simultaneously access the same memory resource.

 

We can use mutex(MUTual EXclusion) in this case.

var mutex sync.Mutex

func testFunc(num *int) {
  mutex.Lock()
  defer mutex.Unlock()

  *num += 1000
  time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
  *num -= 1000
}

We just simply call Lock() method before accessing the shared resources and call Unlock() method after using the shared resources.

 

Mutex can resolve the concurrency problem.

However, we lose the benefit of concurrency programming because only the goroutine that obtained mutex is executed.

And, it may occur a deadlock problem.

So, you need to pay attention to design architecture when you use concurrency programming.

Channel

Channel is a medium that the goroutine use in order to communicate effectively.

Channel is also a message queue between goroutines.

 

Let's learn how to make channel first.

// Method 1
var channel chan int
channel = make(chan int)

// Method 2
channel := make(chan int)

Channel that is not initialized or is zero-value is nil.

 

To send and receive data using a channel we should use the channel operator which is <-.

// Send data to channel
ch <- 7

// Receive data from channel
data := <-ch

 

Noe, let's use a channel.

func usingChannel(ch chan int, value int) {
  ch <- value
}

func main() {
  ch := make(chan int)
  
  go usingChannel(ch, 7)
  
  fmt.Println(<-ch)
  // 7
}

You can use any data type with channel even structure.

 

Channel can be unidirectional.

func sendOnlyChannel(ch chan<- int, value int) {
  ch <- value
}

func main() {
  ch := make(chan<- int)
  
  go sendOnlyChannel(ch, 7)
}

 

You can close an open channel.

func closeChannel(ch chan string, str string) {
  ch <- str
  close(ch)
}

func main() {
  ch := make(chan string)
  
  go closeChannel(ch, "Hello Channel")
  
  value, ok := <-ch
  if !ok {
    fmt.Println("Channel closed")
  }
  
  fmt.Println(value)
  // Hello Channel
}

 

A range loop can be used to iterate over all the values sent through the channel.

func loopChannel(ch chan int) {
  ch <- 1
  ch <- 2
  ch <- 3
  ch <- 4
  ch <- 5
  close(ch)
}

func main() {
  ch := make(chan int)
  
  go loopChannel(ch)
  
  for v := range ch {
    fmt.Println(v)
  }
  // 1
  // 2
  // 3
  // 4
  // 5
}

The channel should be closed after use it.

 

All the channels we've looked at so far were unbuffered channels.

We can make a buffered channel as well.

make(chan int, 5)

Simply specify the size of the buffer.

Others, such as how to use them, are all the same.

Select

Select can wait from multiple channels.

func testFunc(wg *sync.WaitGroup, num chan int, quit chan bool) {
  for {
    select {
    case n := <-num:
      fmt.Printf("Num: %d\n", n)
      time.Sleep(time.Second)
    case <-quit:
      wg.Done()
      return
    }
  }
}

func main() {
  var wg sync.WaitGroup
  num := make(chan int)
  quit := make(chan bool)

  wg.Add(1)
  go testFunc(&wg, num, quit)

  for i := 0; i < 4; i++ {
    num <- i
  }
  quit <- true
  wg.Wait()
}

In the above example, select statement can receive data from num channel and quit channel.

Context

Context is a kind of job specification.

We can use the context to direct the task for a specific time or cancel it externally.

Context With Cancel

var wg sync.WaitGroup

func testFunc(ctx context.Context) {
  tick := time.Tick(time.Second)
  for {
    select {
    case <-ctx.Done():
      wg.Done()
      return
    case <-tick:
      fmt.Println("1s Tick")
    }
  }
}

func main() {
  ctx, cancel := context.WithCancel(context.Background())

  wg.Add(1)
  go testFunc(ctx)
  time.Sleep(5 * time.Second)
  cancel()
  wg.Wait()
}

If we use WithCancel() method, it returns a cancel() function .

The cancel() function can cancel a job that we want to stop.

Context with Timeout

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

If we change the WithCancel() method to WithTimeout() method, the job will be stopped after the set time.

Context with WithValue

var wg sync.WaitGroup

func testFunc(ctx context.Context) {
  if v := ctx.Value("number"); v != nil {
    n := v.(int)
    fmt.Println(n)
  }
  wg.Done()
}

func main() {
  ctx := context.WithValue(context.Background(), "number", 7)

  wg.Add(1)
  go testFunc(ctx)
  wg.Wait()
}

If we use WithValue() method, we can order workers to do the job with a pair of a key and a value.

Nested Context

Context can be nested.

ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, "number", 7)

'Go' 카테고리의 다른 글

[Go] Testing  (0) 2021.10.26
[Go] Error Handling  (0) 2021.10.22
[Go] Map  (0) 2021.10.21
[Go] Linked List  (0) 2021.10.20
[Go] Functions  (0) 2021.10.19

댓글