One of golang’s key features is its support for concurrency, which is implemented through Goroutine. As the name implies, goroutine is the coroutine implemented by Golang.

When we say concurrency, it can be thread concurrency, it can be coroutine concurrency, and the advantage of coroutines over threads is that coroutines are lighter than threads, so concurrency can be higher. In the case of Goroutine, a GO process consists of thousands of Goroutines running simultaneously.

channel

In Golang, each Goroutine is independent, and multiple Goroutines need to communicate in order to accomplish a task together.

For example, task T is jointly completed by two coroutines A and B, and there is A dependency relationship between A and B. Coroutine B depends on the execution result of coroutine A, that is, only after the execution of coroutine A is completed, can coroutine B begin to execute.

Communication between coroutines in Golang is accomplished through channels.

A channel can be thought of as a pipe. Data flows in at one end and out at the other. The semantics of a channel are that when we read data from a pipe, the read will block until data flows into the pipe; Similarly, when we write to a pipe, the write continues to block until the pipe is read away.

unbuffered channel

The semantics of an unbuffered channel are as follows: When we read data from a pipe, the read will block until something enters the pipe; Similarly, when we write to a pipe, the write continues to block until the pipe is read away.

In the following case, I want to print Hello World on the screen before the program exits. To do this, I use a channel variable of type Chan bool called done.

package main

import (
    "fmt"
    "time"
)
// Accept bool cahnnel
func hello(done chan bool) {
    fmt.Println("hello world")
    time.Sleep(4 * time.Second)
    done <- true
}
func main(a) {
  // create a non-nil channel with make
    done := make(chan bool)
    fmt.Println("Main going to call hello go goroutine")
    go hello(done)
    <- done // Pipe reads block until hello Goroutine executes and writes data to the pipe, comment out the line, main Goroutine executes until the end, hello Goroutine is not scheduled
    fmt.Println("Main received data")}Copy the code

buffered channel

Capacity specifies the length of a channel’s buffer. This type of channel is called a buffered channel. Capacity is 0 by default.

Similarly, the semantics of a buffered channel are easy to understand: when the buffer is full, writing will be blocked; When the buffer is empty, further reading is blocked.

package main

import (
    "fmt"
    "time"
)

func write(ch chan int) {
    for i := 0; i < 5; i++ {
        ch <- i
        fmt.Println("successfully wrote", i, "to ch")}close(ch)
}
func main(a) {
    ch := make(chan int.2)
    go write(ch)
    time.Sleep(2 * time.Second)
    for v := range ch {
        fmt.Println("read value", v,"from ch")
        time.Sleep(2 * time.Second)

    }
}

/* successfully wrote 0 to ch successfully wrote 1 to ch read value 0 from ch successfully wrote 2 to ch read value 1 from ch successfully wrote 3 to ch read value 2 from ch successfully wrote 4 to ch read value 3 from ch read value 4 from ch */
Copy the code

mutex

Mutex is essentially a locking mechanism that ensures that only one goroutine can enter the critical section at any one time, thereby preventing race conditions.

In the following case, start 1000 Goroutines to increment x 1000 times. Each run may not be the same, x will be less than or equal to 1000. This is because the increment operation on x is not atomic. At some point, Goroutine1 reads x as 10, and after +1 it reads 11, but it hasn’t written to main memory yet. At that point, a coroutine switch occurs, Goroutine2 runs, Goroutine2 reads from main memory to x is still 10, and after +1 it reads 11. Goroutine1 and Goroutine2 write the word results back to main memory (regardless of order), and the value of x is updated to 11. There are two increment operations that only achieve +1. (Local run)

package main

import (
  "fmt"
  "sync"
)

var x = 0

func increment(wg *sync.WaitGroup, m *sync.Mutex) {
  x = x + 1
  wg.Done()
}
func main(a) {
  var w sync.WaitGroup
  for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w, &m) // Address must be used here
  }
  w.Wait()
  fmt.Println("final value of x", x)
}

Copy the code

The solution to this problem is simply to lock each increment operation before it is executed and release the lock after it is executed to ensure atomicity of the increment operation. The value of x in the following case will be 1000 no matter how many times it is run, which means that the result of the program is certain.

package main

import (
  "fmt"
  "sync"
)

var x = 0

func increment(wg *sync.WaitGroup, m *sync.Mutex) {
  m.Lock()
  x = x + 1
  m.Unlock()
  wg.Done()
}
func main(a) {
  var w sync.WaitGroup
  var m sync.Mutex
  for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w, &m) // Address must be used here
  }
  w.Wait()
  fmt.Println("final value of x", x)
}

Copy the code

In particular, we can use channels to implement mutex functionality.

package main

import (
  "fmt"
  "sync"
)

var x = 0

func increment(wg *sync.WaitGroup, m chan int) {
  m <- 1
  x = x + 1
  <- m
  wg.Done()
}
func main(a) {
  var w sync.WaitGroup
  m := make(chan int.1)
  for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w, m) // Address must be used here
  }
  w.Wait()
  fmt.Println("final value of x", x)
}

Copy the code

WaitGroup

In addition to channels and Mutex, Golang provides WaitGroups and Select for concurrency.

WaitGroup is essentially a counter, and it only goes to the next step if counter=1. Normally we use WaitGroup for semantics: when a set of Goroutines is complete, we proceed to the next step.

In the following case, counter is incremented by 1 before a goroutine is executed, and counter is incremented by 1 before the goroutine exits, ensuring that main Goroutine is executed only after all goroutines have been completed.

package main

import (
    "fmt"
    "sync"
    "time"
)

func process(i int, wg *sync.WaitGroup) {
    fmt.Println("started Goroutine ", i)
    time.Sleep(2 * time.Second)
    fmt.Printf("Goroutine %d ended\n", i)
    wg.Done() // -1
}

func main(a) {
    no := 3
    var wg sync.WaitGroup
    for i := 0; i < no; i++ {
        wg.Add(1) //  + 1
        go process(i, &wg) // WG must use pointer otherwise each Goroutine will have its own WaitGroup
    }
    wg.Wait() // =0 to proceed to next step
    fmt.Println("All go routines finished executing")}Copy the code

Select

The syntax of select is very similar to the syntax of switch. It is used to implement the following semantics: block when all coroutines in a set of coroutines are in block; when one coroutine in the set is ready, that coroutine is selected to execute; when multiple coroutines in the set are ready, one of them is randomly selected to execute.

package main

import (
  "fmt"
  "time"
)

func server1(ch chan string) {
  time.Sleep(6 * time.Second)
  ch <- "from server1"
}
func server2(ch chan string) {
  time.Sleep(3 * time.Second)
  ch <- "from server2"

}
func main(a) {
  output1 := make(chan string)
  output2 := make(chan string)
  go server1(output1)
  go server2(output2)

  // Wait until one of the channels returns, then execute it
  select {
  case s1 := <-output1:
      fmt.Println(s1)
  case s2 := <-output2:
      fmt.Println(s2)
  }
}
Copy the code

Summary and prospects for the next chapter

This article introduces several key concepts of concurrent programming in Golang. The nice thing about Golang is that many of the key concepts in concurrency are very clear from Golang.

The next part introduces key concepts in concurrent programming, how they relate to each other, and how these key concepts are implemented in Golang.

  1. Critical section, race condition
  2. Synchronization primitives, mutex variables, conditional variables, semaphores, locks

reference

  • The code examples for this article are from: Code Examples