Learn about Goroutine and Channel

Goroutine

What is Goroutine

  • Goroutine is a unique concurrency body of Golang. It is a lightweight "thread"
  • The most basic execution unit in Go. Each Goroutine executes independently
  • Each Go program has at least one Goroutine: the main Goroutine. When the program starts, it is created automatically.
func main() {
  say()  // Running, waiting for results
  
  go say()
  fmt.Println("end")  // There is no need to wait for the execution result of say()
}

func say(){
   fmt.Println("hello world")
}

Vs Os Thread

Os Thread

Each system thread has a stack of fixed size, generally 2M by default. This stack is mainly used to save parameters and local variables during recursive function calls, which are scheduled by the kernel

Fixed size stacks can cause problems

  • Waste of space
  • The space may not be enough, and there is a risk of stack overflow

Goroutine

It is scheduled by the Go scheduler. When it is first created, it is very small (2kb or 4kb). It will dynamically scale the size of the stack as needed (the maximum value of the stack in the mainstream implementation can reach 1GB).

Because the cost of starting is very small, we can easily start thousands of goroutines.

Learn by example

1. Multiple goroutine s run simultaneously

The order of operation is determined by the scheduler and does not need to be interdependent

func main() {
   fmt.Println("Started")
   for i := 0; i < 10; i++ {
      go execute(i)
   }
   time.Sleep(time.Second * 1)
   fmt.Println("Finished")
}
func execute(id int) {
   fmt.Printf("id: %d\n", id)
}

2. Concurrent downloading of pictures

func main() {
   urls := []string{
      "https://pic.netbian.com/uploads/allimg/210925/233922-163258436234e8.jpg",
      "https://pic.netbian.com/uploads/allimg/210920/180354-16321322345f20.jpg",
      "https://pic.netbian.com/uploads/allimg/210916/232432-16318058722f4d.jpg",
   }
   for _,url := range urls{

      go downloadFile(url)
   }
   time.Sleep(time.Second)
}

func downloadFile(URL string) error {
   //Get the response bytes from the url
   response, err := http.Get(URL)
   if err != nil {
      return err
   }
   defer response.Body.Close()

   if response.StatusCode != 200 {
      return errors.New("Received non 200 response code")
   }
   //Create a empty file
   file, err := os.Create(path.Base(URL))
   if err != nil {
      return err
   }
   defer file.Close()

   //Write the bytes to the fiel
   _, err = io.Copy(file, response.Body)
   if err != nil {
      return err
   }

   return nil
}

Recover

Each Goroutine must have a recover y mechanism, because when a Goroutine throws a panic, only it can catch it, and other goroutines can't catch it.

If there is no recover y mechanism, the whole process will crash.

Note: when a panic occurs in Goroutine, only its own defer will be called, so even if the recover logic is written in the main Goroutine, it cannot recover.

func main() {
    go do1()
    go do2()
    time.Sleep(10*time.Second)
}

func do1() {
    for i := 0; i < 100; i++ {
        fmt.Println("do1", i)
    }
}

func do2() {

    defer func() {
        if err := recover(); err != nil {
            log.Printf("recover: %v", err)
        }
    }()

    for i := 0; i < 100; i++ {
        if i ==5{
            panic("do panic")
        }
        fmt.Println("do2", i)
    }
}

Channel

Basic introduction

Channel is the built-in data type of Go. The value of the initialized channel is nil

Communicate by sending and receiving values of the specified element type

  • Channel provides synchronization and communication between goroutines
  • Goroutine implements concurrent / parallel lightweight independent execution.

Shard Memory

graph
thread1 --> Memory
thread2 --> Memory
thread3 --> Memory

CSP

Communication sequential processes

It is used to describe the concurrency model of two independent concurrent entities communicating through a shared communication channel,

It does not focus on the entity sending the message, but on the channel used when sending the message

Do not communicate through shared memory, but share memory through communication-- Rob Pike

graph LR
Goroutine1 --> Channel --> Goroutine2

data structure

type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint           // size of the circular queue
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemsize uint16
    closed   uint32         // denotes weather channel is closed or not
    elemtype *_type         // element type
    sendx    uint           // send index
    recvx    uint           // receive index
    recvq    waitq          // list of recv waiters
    sendq    waitq          // list of send waiters
    lock     mutex
}

Basic Usage

definition

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

< - operator specifies the channel direction, send or receive. If no direction is given, the channel is bidirectional

chan T          // Can send receive T
chan<- T        // Only T can be sent
<-chan T        // Only T can be received

establish

ch := make(chan int)     // Unbuffered cap 0
ch := make(chan int,100) // Buffered cap 100

operation

ch <- 1. // send out
<-ch.    // receive
close(ch)// close

Code example

func goroutineA(ch <-chan int) {
   fmt.Println("[goroutineA] want a data")
   val := <-ch
   fmt.Println("[goroutineA] received the data", val)
}

func goroutineB(ch chan<- int) {
   ch <- 1
   fmt.Println("[goroutineB] send the data 1")
}

func main() {
   ch := make(chan int)
   go goroutineA(ch)
   go goroutineB(ch)
   time.Sleep(time.Second)
}
sequenceDiagram
groutineA->channel: hello,I want to get a data
channel-->groutineA: I don't have data yet
groutineA->channel: Then I'll go to bed and wake me up when there's data
channel-->groutineA: ok
groutineB->channel: hello,I want to send you a data
channel-->groutineB: ok,Send it
channel->groutineA: Wake up and receive the data
groutineA-->channel: Come on

Unbuffered channels

channel with buffer size 0

The channel receiver blocks until the message is received, and the channel sender blocks until the receiver receives the message

Buffered channels

Have a buffer. When the buffer is full, the sender will block; When the buffer is empty, the receiver blocks

summary

Do not pay attention to the data structure of the channel, but pay more attention to the behavior of the channel

Commandnilemptyfullnot full & emptyclosed
Receiveblockblocksuccesssuccesssuccess
Sendblocksuccessblocksuccesspanic
Closepanicsuccesssuccesssuccesspanic

Several principles

  • The sending operation on the channel always occurs before the corresponding receiving operation is completed
  • If the channel receives data from it after it is closed, the receiver will receive the zero value returned by the channel
  • The reception from an unbuffered channel occurs before the transmission to the channel is completed
  • Do not close the channel when there are data receivers or multiple senders. In other words, we should only let the only sender of a channel close the channel

Example

package main

import "fmt"

func main() {
    ch1 := make(chan string)
    ch1 <- "hello world"
    fmt.Println(<-ch1)
}

An error will be reported after execution

fatal error: all goroutines are asleep - deadlock!

reason?

In line 7, the value hello world is passed to channel ch1. However, for non buffered channels, the sending operation is blocked before the receiver is ready, and the lack of receiver causes deadlock

How to solve it?

1. Add receiver
func main() {

    ch1 := make(chan string)
    go func() {
        fmt.Println(<-ch1)
    }()
    ch1 <- "hello world"
    time.Sleep(time.Millisecond)
}

func main() {
    ch1 := make(chan string)
    go func() {
        ch1 <- "hello world"
    }()
    fmt.Println(<-ch1)
}
2. Increase channel capacity
func main() {

    ch1 := make(chan string,1)
    ch1 <- "hello world"
    fmt.Println(<-ch1)
}

Goroutine & Channel

Common concurrency patterns

notice

  1. Send a value implementation notification to a channel
func main() {
   ch := make(chan int)
   go do(ch)
   // do something
   <- ch
   fmt.Println("done")
}

func do(ch chan int){
   // Long time operation
   time.Sleep(3*time.Second)
   fmt.Println("doing")
   ch <- 1
}
  1. Receive value implementation notifications from a channel
func main() {
   ch := make(chan int)

   go do(ch)
   // do something
   ch <- 1
   fmt.Println("done")
}

func do(ch chan int){
   // Long time operation
   time.Sleep(3*time.Second)
   fmt.Println("doing")
   <-ch
}

mutex

func main() {
   mutex := make(chan struct{}, 1) // Capacity must be 1

   counter := 0
   increase := func() {
      mutex <- struct{}{} // Lock
      counter++
      <-mutex // Unlock
   }

   increase1000 := func(done chan<- struct{}) {
      for i := 0; i < 1000; i++ {
         increase()
      }
      done <- struct{}{}
   }

   done := make(chan struct{})
   go increase1000(done)
   go increase1000(done)
   <-done; <-done
   fmt.Println(counter) // 2000
}

Controls the number of concurrent processes

Unrestricted scenarios

func main() {
   for i := 0; i < math.MaxInt32; i++ {
      go func(i int) {

         log.Println(i)
         time.Sleep(time.Second)
      }(i)
   }

   for {
         time.Sleep(time.Second)
     }
}

Operation results

$ go run main.go
...
150577
150578
panic: too many concurrent operations on a single file or socket (max 1048575)

Question: how to limit the number of collaborative processes?

// main_chan.go
func main() {
    ch := make(chan struct{}, 4)

    for i := 0; i < 20; i++ {
        ch <- struct{}{}
        go func(i int) {

            log.Println(i)
            time.Sleep(time.Second)
            <-ch
        }(i)
    }

    for {
        time.Sleep(time.Second)
    }
}

Producer consumer model

// Producer: generates a sequence of factor integer multiples
func Producer(factor int, out chan<- int) {
    for i := 0; i < 10; i++ {
        out <- i * factor
    }
}

// consumer
func Consumer(in <-chan int) {
    for v := range in {
        fmt.Println(v)
    }
}
func main() {
    ch := make(chan int, 3) // Achievement queue

    go Producer(3, ch) // Generate a sequence of multiples of 3
    go Producer(5, ch) // Generate a sequence of multiples of 5
    go Consumer(ch)    // Consumption generated queue

    time.Sleep(5 * time.Second)
}

Return optimal results

func main() {
   ch := make(chan string, 32)

   go func() {
      ch <- searchByGoogle("golang")
   }()
   go func() {
      ch <- searchByBaidu("golang")
   }()

   fmt.Println(<-ch)
}

func searchByGoogle(search string) string {
   time.Sleep(2 * time.Second)
   return "google result: " + search
}

func searchByBaidu(search string) string {
   time.Sleep(time.Second)
   return "baidu result " + search
}
Question 1:

When the desired results are obtained, how to notify or safely exit other ongoing processes?

func main() {
    ch := make(chan string, 32)
    cancel := make(chan struct{},2)
    
    go func() {
        ch <- searchByGoogle("golang",cancel)
    }()
    go func() {
        ch <- searchByBaidu("golang",cancel)
    }()

    fmt.Println(<-ch)
    cancel <- struct{}{}

    time.Sleep(time.Second)
}

func searchByGoogle(search string,cancel chan struct{}) string {

    done := make(chan struct{})
    go func() {
        time.Sleep(2 * time.Second)
        done <- struct{}{}
    }()

    select {
    case <- done:
        return "google result " + search
    case <- cancel:
        fmt.Println("google cancel")
        return "google cancel"
    }
}

func searchByBaidu(search string,cancel chan struct{}) string {
    done := make(chan struct{})
    go func() {
        time.Sleep(1 * time.Second)
        done <- struct{}{}
    }()

    select {
    case <- done:
        return "baidu result " + search
    case <- cancel:
        fmt.Println("google cancel")
        return "baidu cancel"
    }
}
Question 2:

How to do timeout control?

Goroutine leak

1. Forgotten sender

func searchByBaidu(search string,cancel chan struct{}) string {
    done := make(chan struct{}) 
    go func() {
        time.Sleep(1 * time.Second)
        done <- struct{}{}
    }()

    select {
    case <- done:
        return "baidu result " + search
    case <- cancel:
        fmt.Println("google cancel")
        return "baidu cancel"
    }
}

Case < - done and case < - cancel are not sure which will be executed. If < - cancel is executed, the fifth line done < - struct {} {} will be blocked forever, and Goroutine cannot exit

How to solve it?

Increase channel capacity

done := make(chan struct{})

Is there any other way?

func searchByBaidu(search string,cancel chan struct{}) string {
   done := make(chan struct{})
   go func() {
      time.Sleep(1 * time.Second)
      select {
      case done <- struct{}{}:
      default:
         return
      }
   }()

   select {
   case <- done:
      return "baidu result " + search
   case <- cancel:
      fmt.Println("google cancel")
      return "baidu cancel"
   }
}

2. Forgotten recipients

import (
   "errors"
   "fmt"
   "io"
   "net/http"
   "os"
   "path"
   "runtime"
)

type result struct {
   url string
   err error
}

func main() {

   startGNum := runtime.NumGoroutine()
   urls := []string{
      "https://pic.netbian.com/uploads/allimg/210925/233922-163258436234e8.jpg",
      "https://pic.netbian.com/uploads/allimg/210920/180354-16321322345f20.jpg",
      "https://pic.netbian.com/uploads/allimg/210916/232432-16318058722f4d.jpg",
   }

   total := len(urls)
   // Fill input
   input := make(chan string, total)
   for _, url := range urls {
      input <- url
   }
   // close(input)
   output := make(chan *result, total)
   // Start 4 goroutine s
   for i := 0; i < 4; i++ {
      go download(input, output)
   }
   // Waiting for results
   for i := 0; i < total; i++ {
      ret := <-output
      fmt.Println(ret.url, ret.err)
   }
   time.Sleep(2*time.Second)  // Wait for the exit of the download process
   endGNum := runtime.NumGoroutine()
   fmt.Println("start goroutine", startGNum)
   fmt.Println("end goroutine", endGNum)
}

func download(input <-chan string, output chan<- *result) {

   for v := range input {
      err := downloadFile(v)
      output <- &result{
         url: v,
         err: err,
      }
   }
   fmt.Println("download finish!!!")
}

func downloadFile(URL string) error {
   //Get the response bytes from the url
   response, err := http.Get(URL)
   if err != nil {
      return err
   }
   defer response.Body.Close()

   if response.StatusCode != 200 {
      return errors.New("Received non 200 response code")
   }
   //Create a empty file
   file, err := os.Create(path.Base(URL))
   if err != nil {
      return err
   }
   defer file.Close()

   //Write the bytes to the fiel
   _, err = io.Copy(file, response.Body)
   if err != nil {
      return err
   }

   return nil
}

This will cause Goroutine leakage. The reason is that for V: = range input in line 49 continues to wait for input when there is no data input. All you should tell it when there is no data input and let it not wait foolishly

How to solve it?

  1. Close the input channel
  2. Pass a channel to tell it the result

principle

  1. Never start Goroutine without knowing how to stop. When we start a Goroutine, we need to consider several issues

    • When will it stop?
    • How can it be terminated?
  2. Leave concurrency to the caller

    • Please give the caller the option of whether to call asynchronously, otherwise the caller may not know that you used Goroutine in this function
    • If your function starts a Goroutine, you must provide the caller with a way to explicitly stop the Goroutine. It is usually easier to leave the decision to execute a function asynchronously to the caller of the function.

summary

Concurrency is a useful tool, but it must be used with caution.

Concurrency is a useful tool, but it must be used with caution

Reference link

Tags: Go Back-end Channel goroutine

Posted on Mon, 29 Nov 2021 01:14:52 -0500 by FatalError