并發是 golang 的優勢之一,使用關鍵字 go 可以很友善的開啟一個協程. go 語言中,常常用 go、chan、select 及 sync 庫完成并發操作,處理同步、異步、阻塞、非阻塞任務.
1. 概要
go 語言的并發程式設計,以下是需要了解的基礎知識點,也是本文主要介紹的内容. 可以對照看看這些是否已經可以熟練運用了.
- 阻塞 : 阻塞是程序(也可以是線程、協程)的狀态之一(建立、就緒、運作、阻塞、終止). 指的是當資料未準備就緒,這個程序(線程、協程)一直等待,這就是阻塞.
- 非阻塞 : 當資料為準備就緒,該程序(線程、協程)不等待可以繼續執行,這就是非阻塞.
- 同步 : 在發起一個調用時,在沒有得到結果之前,這個調用就不傳回,這個調用過程一直在等待. 這是同步.
- 異步 : 在發起調用後,就立刻傳回了,這次調用過程就結束了. 等到有結果了被調用方主動通知調用者結果. 這是異步.
- go(協程) : 通過關鍵字 go 即可建立一個協程.
- chan : golang 中用于并發的通道,用于協程的通信.
- 有緩沖通道
- 無緩沖通道
- 單向通道
- select : golang 提供的多路複用機制.
- close() : golang 的内置函數, 可以關閉一個通道.
- sync : golang 标準庫之一,提供了鎖.
- 定時器 : golang 标準庫 time 提供的重要功能, 提供了定時器功能,可用于逾時處理.
- Timer
- Ticker
2. go 并發程式設計
go 的并發程式設計采用的 CSP (Communicating Sequential Process) 模型,主要基于協程 goroutine 和通道 channel .
2.1 協程 go在 go 語言中,并發程式設計使用關鍵字 go 即可快速啟動一個并發運作的 goroutine. 如下:
go 函數名 (參數清單)
go f(a int, b int, c int){
fmt.Println(a+b+c)
}(1,2,3)
2.2 channel 通道 golang 提供了通道類型 chan,用于在并發操作時的通信,它本身就是并發安全的. 通過 chan 可以建立無緩沖、緩沖通道,滿足不同需求. 寫法如下:
make(chan int)
make(chan int, 10)
<- chan
chan <-
無緩沖通道: 要求接受和發送資料的 goroutine 同時準備好,否則将會阻塞.
有緩沖通道:給予通道一個容量值,隻要有值便可以接受資料,有空間便可以發送資料,可以不阻塞的完成.
單向通道: 預設情況通道是雙向的,可以接受及發送資料. 也可以建立單向通道,隻能收或者發資料. 如下是單向接受通道
var ch chan
<- float64
2.3 select select: 可以監聽 channel 上的輸入/輸出操作, 類似于 select、epoll、poll 使得通道支援多路複用. select 是專門通道 channel 設計的. 它可以結合通道實作
逾時處理、判斷緩沖通道是否阻塞、退出信号量處理,如下:
// 1. 逾時機制
select {
case <-ch:
case <-timeout:
fmt.Println("timeout 01")}// 2. 退出信号量處理select { case <- quitChan: return default:}// 3. 判斷緩沖通道是否已滿ch := make(chan int, 5)ch <- 1select { case ch <- 2: fmt.Println("channel value is", <-ch) default: fmt.Println("channel blocking")}
2.4 内置函數 close() close() 函數用于關閉通道 channel 的,close 之後的 channel 還可以讀取資料,close() 函數由以下幾點使用要點:
- 隻能關閉雙向通道或者發送通道
- 它應該由發送者使用,而不應該由接受者調用
- 當通道關閉後,接受者都不再阻塞,
- 關閉通道後,依然可以從通道中讀取值
- 所有元素讀取完後,将傳回通道元素的零值,并且讀取檢測值也是 false
示例:
ch := make(chan int, 1)
ch <- 3
close(ch) // 關閉ch
v, ok := <- ch // 3,true
v2,ok := <- ch // 0,false
3. 阻塞、同步與異步
3.1 阻塞與非阻塞程序狀态
阻塞: 阻塞是程序(也可以是線程、協程)的狀态之一(建立、就緒、運作、阻塞、終止). 指的是當資料未準備就緒,這個程序(線程、協程)一直等待,這就是阻塞.
非阻塞: 當資料為準備就緒,該程序(線程、協程)不等待可以繼續執行,這就是非阻塞.
3.2 同步與異步 同步: 在發起一個調用時,在沒有得到結果之前,這個調用就不傳回,這個調用過程一直在等待. 這是同步.
異步: 在發起調用後,就立刻傳回了,這次調用過程就結束了. 等到有結果了被調用方主動通知調用者結果. 這是異步.
3.3 四種組合同步、異步、阻塞、非阻塞可以組合成四種并發方式:
- 同步阻塞調用 :得不到結果不傳回,線程進入阻塞态等待。
- 同步非阻塞調用 :得不到結果不傳回,線程不阻塞一直在CPU運作。
- 異步阻塞調用 :去到别的線程,讓别的線程阻塞起來等待結果,自己不阻塞。
- 異步非阻塞調用 :去到别的線程,别的線程一直在運作,直到得出結果。
4. golang 标準庫的鎖與定時器
4.1 鎖與 sync 庫并發程式設計中,為了確定并發安全,可以使用鎖機制. golang 提供了标準庫 sync ,它實作了并發需要的各種鎖. 包括:
- Mutex : 互斥鎖,有倆個方法 Lock() 和 Unlock() , 它隻能同時被一個 goroutine 鎖定,其它鎖再次嘗試鎖定将被阻塞,直到解鎖.
- RWMutex : 讀寫鎖,有四個方法, Lock() 寫鎖定、 Unlock() 寫解鎖、 RLock() 讀鎖定、 RUnlock() 讀解鎖,讀鎖定和寫鎖定隻能同時存在一個. 隻能有一個協程處于寫鎖定狀态,但是可以有多個協程處于讀鎖定狀态. 即寫的時候不可讀,讀的時候不可寫. 隻能同時有一個寫操作確定資料一緻性. 而可以多個協程同時讀資料,確定讀操作的并發性能.
此外在 go 的并發程式設計中,還會常用到 sync 的以下内容:
- sync.Map : 并發安全的字典 map
- sync.WaitGroup : 用來等待一組協程的結束,常常用來阻塞主線程.
- sync.Once : 用于控制函數隻能被使用一次,
- sync.Cond : 條件同步變量. 可以通過 Wait()方法阻塞協程,通過 Signal()、Broadcast() 方法喚醒協程.
- sync.Pool : 一組臨時對象的集合,是并發安全的. 它主要是用于存儲配置設定但還未被使用的值,避免頻繁的重新配置設定記憶體,減少 gc 的壓力.
golang 的标準庫 time 中提供了定時器功能,并提供通道 channel 變量進行定時通知. time 庫中提供了兩種定時器:
- time.Timer: 定時器 timer 在建立指定時間後,向通道 time.Timer.C 發送資料. 之後需要使用 Reset 設定定時器時間.
- time.Ticker: 周期性定時器. 會按照初設定的時間重複計時.
// timer
for {
<- timer.C
timer.Reset(time.Second)
// 重設後才有效
}
// ticker
for {
<- ticker.C // 周期性有效
}
5. 結語
5.1 思考題1. golang 中 select 的多個 case 同時成立,那麼選擇的是哪一個?
2. golang 中除了使用 sync 鎖,還可以如何保證并發安全? atomic 是什麼?
3. sync.Map 對鍵的類型有什麼要求麼?
4. 如何避免死鎖? golang 中如何檢測死鎖?
5.2 參考資料1. Golang 并發程式設計 [https://www.cnblogs.com/konghui/p/10703615.html#close]
2. 深入了解并發/并行,阻塞/非阻塞,同步/異步[https://cloud.tencent.com/developer/article/1339622]