Go Concurrency

Concurrency is the ability of a program to execute multiple tasks simultaneously.

The Go language supports concurrency, providing a concise and efficient way to implement it through goroutines and channels.

Goroutines:

  • The unit of concurrent execution in Go, similar to lightweight threads.
  • Goroutine scheduling is managed by the Go runtime; users do not need to manually allocate threads.
  • UsagegoThe keyword starts a Goroutine.
  • Goroutines are non-blocking and can efficiently run tens of thousands of goroutines.

Channel:

  • The mechanism used for communication between Goroutines in Go.
  • Supports synchronization and data sharing, avoiding explicit locking mechanisms.
  • UsagechanCreated by the keyword, through<-The operator sends and receives data.

Scheduler:

Go's scheduler is based on the GMP model. The scheduler assigns Goroutines to system threads for execution, and efficiently manages concurrency through the coordination of M and P.

  • G:Goroutine。
  • M: System thread (Machine).
  • P: Logical processor.

Goroutine

Goroutines are lightweight threads, and their scheduling is managed by the Go runtime.

Goroutine syntax format:

go 函数名( 参数列表 )

For example:

go f(x, y, z)

Start a new goroutine:

f(x, y, z)

Go allows the go statement to start a new runtime thread, namely a goroutine, executing a function in a distinct, newly created goroutine. All goroutines in the same program share the same address space.

Example

package main

import (
        "fmt"
        "time"
)

func sayHello() {
        for i := 0; i < 5; i++ {
                fmt.Println("Hello")
                time.Sleep(100 * time.Millisecond)
        }
}

func main() {
        go sayHello() // Start a goroutine
        for i := 0; i < 5; i++ {
                fmt.Println("Main")
                time.Sleep(100 * time.Millisecond)
        }
}

Executing the above code, you will see Main and Hello in the output. The output has no fixed order because they are executed by two goroutines:

Main
Hello
Main
Hello
...

Channel

Channels are used for data transfer between Goroutines.

Channels can be used for synchronized execution and communication between two goroutines by passing a value of a specified type.

UsagemakeThe function creates a channel, using<-The <- operator sends and receives data. If no direction is specified, it is a bidirectional channel.

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据
           // 并把值赋给 v

Declaring a channel is very simple; we just use the chan keyword. A channel must be created before use:

ch := make(chan int)

NoteBy default, channels are unbuffered. When the sender sends data, there must be a corresponding receiver receiving the data at the same time.

The following example uses two goroutines to calculate the sum of numbers. After the goroutines complete their calculations, it calculates the sum of the two results:

Example

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // Send sum to channel c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // Receive from channel c

    fmt.Println(x, y, x+y)
}

The output result is:

-5 17 12

Channel Buffer

Channels can have a buffer, with the buffer size specified by the second argument of make:

ch := make(chan int, 100)

Buffered channels allow the sender's data sending and the receiver's data receiving to be in an asynchronous state. That is, data sent by the sender can be placed in the buffer and wait for the receiver to retrieve it, rather than requiring the receiver to retrieve it immediately.

However, since the buffer size is limited, there must still be a receiver to receive data. Otherwise, once the buffer is full, the sender cannot send more data.

NoteIf a channel is unbuffered, the sender blocks until the receiver has received a value from the channel. If a channel is buffered, the sender blocks until the sent value is copied into the buffer; if the buffer is full, it means waiting until some receiver retrieves a value. The receiver blocks until there is a value to receive.

Example

package main

import "fmt"

func main() {
    // Here we define a buffered channel that can store integers
    // The buffer size is 2
    ch := make(chan int, 2)

    // Because ch is a buffered channel, we can send two values simultaneously
    // without needing to read data synchronously immediately
    ch <- 1
    ch <- 2

    // Receive these two values
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

The execution output is:

1
2

Go ranging over channels and closing channels

Go uses the range keyword to iterate over received data, similar to arrays or slices. The format is as follows:

v, ok := <-ch

If the channel receives no data, ok becomes false, and then the channel can be closed using close().close()function to close.

Example

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    // The range function iterates over each value received from the channel, because after c has sent 10
    // values, it closes the channel, so here the range function, after receiving 10 values
    // then ends. If the c channel above is not closed, then the range function will not
    // end, and thus it blocks when receiving the 11th value.
    for i := range c {
        fmt.Println(i)
    }
}

The execution output is:

0
1
1
2
3
5
8
13
21
34

Select statement

selectThe select statement allows a goroutine to wait for multiple communication operations.selectIt will block until one of its cases can proceed:

Example

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

In the above code,fibonaccigoroutine on channelcSend the Fibonacci sequence on, and when receivingquitExit when a signal is received from the channel.

The execution output is:

0
1
1
2
3
5
8
13
21
34
quit

Using WaitGroup

sync.WaitGroup is used to wait for multiple Goroutines to complete.

Synchronizing multiple goroutines:

Example

package main

import (
        "fmt"
        "sync"
)

func worker(id int, wg *sync.WaitGroup) {
        defer wg.Done() // Call Done() when the goroutine completes
        fmt.Printf("Worker %d started\n", id)
        fmt.Printf("Worker %d finished\n", id)
}

func main() {
        var wg sync.WaitGroup

        for i := 1; i <= 3; i++ {
                wg.Add(1) // Increment the counter
                go worker(i, &wg)
        }

        wg.Wait() // Wait for all goroutines to complete
        fmt.Println("All workers done")
}

For the above code, the execution output is as follows:

Worker 1 started
Worker 1 finished
Worker 2 started
Worker 2 finished
Worker 3 started
Worker 3 finished
All workers done

Advanced Features

Buffered Channel:

Create a buffered channel.

ch := make(chan int, 2)

Context:

Used to control the lifecycle of Goroutines.

context.WithCancel、context.WithTimeout。

Mutex and RWMutex:

sync.Mutex provides a mutual exclusion lock for protecting shared resources.

var mu sync.Mutex
mu.Lock()
// critical section
mu.Unlock()

Summary of Concurrent Programming

The Go language provides powerful concurrency support through Goroutines and Channels, simplifying the complexity of the traditional threading model. With the scheduler and synchronization tools, high-performance concurrent programs can be easily implemented.

  • GoroutinesIs a lightweight thread, usinggoStarted by the keyword.
  • ChannelsUsed for communication between goroutines.
  • Select statementUsed to wait for multiple channel operations.

FAQ

Deadlock:

  • Example: All goroutines are waiting, but no data is available.
  • Solution: Avoid infinite waiting and close channels properly.

Data Race:

  • Example: Multiple goroutines access the same variable simultaneously.
  • Solution: use Mutex or Channel to synchronize access.
other extensions