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.
- Usage
goThe 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.
- Usage
chanCreated 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
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
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
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
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
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
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, using
goStarted 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.