Go 併發編程:goroutine,channel 和 sync
優雅的併發編程範式,完善的併發支持,出色的併發性能是 Go 語言區別於其他語言的一大特色。
在當今這個多核時代,併發編程的意義不言而喻。使用 Go 開發併發程序,操作起來非常簡單,語言級別提供關鍵字 go
用於啓動協程,並且在同一臺機器上可以啓動成千上萬個協程。
下面就來詳細介紹。
goroutine
Go 語言的併發執行體稱爲 goroutine,使用關鍵詞 go
來啓動一個 goroutine。
go
關鍵詞後面必須跟一個函數,可以是有名函數,也可以是無名函數,函數的返回值會被忽略。
go
的執行是非阻塞的。
先來看一個例子:
package main
import (
"fmt"
"time"
)
func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n)
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) // Fibonacci(45) = 1134903170
}
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
從執行結果來看,成功計算出了斐波那契數列的值,說明程序在 spinner
處並沒有阻塞,而且 spinner
函數還一直在屏幕上打印提示字符,說明程序正在執行。
當計算完斐波那契數列的值,main
函數打印結果並退出,spinner
也跟着退出。
再來看一個例子,循環執行 10 次,打印兩個數的和:
package main
import "fmt"
func Add(x, y int) {
z := x + y
fmt.Println(z)
}
func main() {
for i := 0; i < 10; i++ {
go Add(i, i)
}
}
有問題了,屏幕上什麼都沒有,爲什麼呢?
這就要看 Go 程序的執行機制了。當一個程序啓動時,只有一個 goroutine 來調用 main
函數,稱爲主 goroutine。新的 goroutine 通過 go
關鍵詞創建,然後併發執行。當 main
函數返回時,不會等待其他 goroutine 執行完,而是直接暴力結束所有 goroutine。
那有沒有辦法解決呢?當然是有的,請往下看。
channel
一般寫多進程程序時,都會遇到一個問題:進程間通信。常見的通信方式有信號,共享內存等。goroutine 之間的通信機制是通道 channel。
使用 make
創建通道:
ch := make(chan int) // ch 的類型是 chan int
通道支持三個主要操作:send
,receive
和 close
。
ch <- x // 發送
x = <-ch // 接收
<-ch // 接收,丟棄結果
close(ch) // 關閉
無緩衝 channel
make
函數接受兩個參數,第二個參數是可選參數,表示通道容量。不傳或者傳 0 表示創建了一個無緩衝通道。
無緩衝通道上的發送操作將會阻塞,直到另一個 goroutine 在對應的通道上執行接收操作。相反,如果接收先執行,那麼接收 goroutine 將會阻塞,直到另一個 goroutine 在對應通道上執行發送。
所以,無緩衝通道是一種同步通道。
下面我們使用無緩衝通道把上面例子中出現的問題解決一下。
package main
import "fmt"
func Add(x, y int, ch chan int) {
z := x + y
ch <- z
}
func main() {
ch := make(chan int)
for i := 0; i < 10; i++ {
go Add(i, i, ch)
}
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
}
可以正常輸出結果。
主 goroutine 會阻塞,直到讀取到通道中的值,程序繼續執行,最後退出。
緩衝 channel
創建一個容量是 5 的緩衝通道:
ch := make(chan int, 5)
緩衝通道的發送操作在通道尾部插入一個元素,接收操作從通道的頭部移除一個元素。如果通道滿了,發送會阻塞,直到另一個 goroutine 執行接收。相反,如果通道是空的,接收會阻塞,直到另一個 goroutine 執行發送。
有沒有感覺,其實緩衝通道和隊列一樣,把操作都解耦了。
單向 channel
類型 chan<- int
是一個只能發送的通道,類型 <-chan int
是一個只能接收的通道。
任何雙向通道都可以用作單向通道,但反過來不行。
還有一點需要注意,close
只能用在發送通道上,如果用在接收通道會報錯。
看一個單向通道的例子:
package main
import "fmt"
func counter(out chan<- int) {
for x := 0; x < 10; x++ {
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}
func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
n := make(chan int)
s := make(chan int)
go counter(n)
go squarer(s, n)
printer(s)
}
sync
sync 包提供了兩種鎖類型:sync.Mutex
和 sync.RWMutex
,前者是互斥鎖,後者是讀寫鎖。
當一個 goroutine 獲取了 Mutex
後,其他 goroutine 不管讀寫,只能等待,直到鎖被釋放。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mutex sync.Mutex
wg := sync.WaitGroup{}
// 主 goroutine 先獲取鎖
fmt.Println("Locking (G0)")
mutex.Lock()
fmt.Println("locked (G0)")
wg.Add(3)
for i := 1; i < 4; i++ {
go func(i int) {
// 由於主 goroutine 先獲取鎖,程序開始 5 秒會阻塞在這裏
fmt.Printf("Locking (G%d)\n", i)
mutex.Lock()
fmt.Printf("locked (G%d)\n", i)
time.Sleep(time.Second * 2)
mutex.Unlock()
fmt.Printf("unlocked (G%d)\n", i)
wg.Done()
}(i)
}
// 主 goroutine 5 秒後釋放鎖
time.Sleep(time.Second * 5)
fmt.Println("ready unlock (G0)")
mutex.Unlock()
fmt.Println("unlocked (G0)")
wg.Wait()
}
RWMutex
屬於經典的單寫多讀模型,當讀鎖被佔用時,會阻止寫,但不阻止讀。而寫鎖會阻止寫和讀。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var rwMutex sync.RWMutex
wg := sync.WaitGroup{}
Data := 0
wg.Add(20)
for i := 0; i < 10; i++ {
go func(t int) {
// 第一次運行後,寫解鎖。
// 循環到第二次時,讀鎖定後,goroutine 沒有阻塞,同時讀成功。
fmt.Println("Locking")
rwMutex.RLock()
defer rwMutex.RUnlock()
fmt.Printf("Read data: %v\n", Data)
wg.Done()
time.Sleep(2 * time.Second)
}(i)
go func(t int) {
// 寫鎖定下是需要解鎖後才能寫的
rwMutex.Lock()
defer rwMutex.Unlock()
Data += t
fmt.Printf("Write Data: %v %d \n", Data, t)
wg.Done()
time.Sleep(2 * time.Second)
}(i)
}
wg.Wait()
}
總結
併發編程算是 Go 的特色,也是核心功能之一了,涉及的知識點其實是非常多的,本文也只是起到一個拋磚引玉的作用而已。
本文開始介紹了 goroutine 的簡單用法,然後引出了通道的概念。
通道有三種:
-
無緩衝通道
-
緩衝通道
-
單向通道
最後介紹了 Go 中的鎖機制,分別是 sync 包提供的 sync.Mutex
(互斥鎖) 和 sync.RWMutex
(讀寫鎖)。
goroutine 博大精深,後面的坑還是要慢慢踩的。
文章中的腦圖和源碼都上傳到了 GitHub,有需要的同學可自行下載。
地址: https://github.com/yongxinz/gopher/tree/main/sc
關注公衆號 AlwaysBeta,回覆「goebook」領取 Go 編程經典書籍。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/VG4CSfT2OfxA6nfygWLSyw