[TOC]
有時候我們的代碼中可能會存在多個 goroutine 同時操作一個資源(臨界區)的情況,這種情況下就會發生競態問題(數據競態)。這就好比現實生活中十字路口被各個方向的汽車競爭,還有火車上的衛生間被車廂里的人競爭。
示例如下:
```go
package main
import (
"fmt"
"sync"
)
var (
total int
swg sync.WaitGroup
)
func sum(j int) {
defer swg.Done()
for i := 0; i < j; i++ {
total += i
}
}
func main() {
swg.Add(2)
go sum(20000)
go sum(20000)
swg.Wait()
fmt.Printf("total: %v\n", total)
}
// 運行10次的結果:
// total: 337931955
// total: 228638665
// total: 256345036
// total: 378517386
// total: 283447322
// total: 366658153
// total: 248299535
// total: 251935294
// total: 399980000
// total: 313504210
```
>[info] 出現上面的結果出現累加和不一致。是因為開了兩個協程執行 sum 函數,當兩個協程同時執行 `total += i` 時,兩個讀到的 total 變量指都是一樣的,加上i的值。所以啟動一個加的i就丟失了, 出現上面的狀況...
## 互斥鎖
互斥鎖是一種常用的控制共享資源訪問的方法,它能夠保證同一時間只有一個 `goroutine` 可以訪問共享資源。Go 語言中使用 `sync` 包中提供的 `Mutex` 類型來實現互斥鎖。
`sync.Mutex` 提供了兩個方法供我們使用。
| 方法名 | 功能 |
| :-: | :-: |
| func (m *Mutex) Lock() | 獲取互斥鎖 |
| func (m *Mutex) Unlock() | 釋放互斥鎖 |
優化上述示例的代碼
```go
package main
import (
"fmt"
"sync"
)
var (
total int
swg sync.WaitGroup
sm sync.Mutex
)
func sum(j int) {
defer swg.Done()
for i := 0; i < j; i++ {
sm.Lock()
total += i
sm.Unlock()
}
}
func main() {
for i := 0; i < 2; i++ {
swg.Add(1)
go sum(20000)
}
swg.Wait()
fmt.Printf("total: %v\n", total)
}
// 運行10次的結果:
// total: 399980000
// total: 399980000
// total: 399980000
// total: 399980000
// total: 399980000
// total: 399980000
// total: 399980000
// total: 399980000
// total: 399980000
// total: 399980000
```
使用互斥鎖能夠保證同一時間有且只有一個 goroutine 進入臨界區,其他的 goroutine 則在等待鎖;當互斥鎖釋放后,等待的 goroutine 才可以獲取鎖進入臨界區,多個 goroutine 同時等待一個鎖時,喚醒的策略是隨機的。
## 讀寫互斥鎖
互斥鎖是完全互斥的,但是實際上有很多場景是讀多寫少的,當我們并發的去讀取一個資源而不涉及資源修改的時候是沒有必要加互斥鎖的,這種場景下使用讀寫鎖是更好的一種選擇。讀寫鎖在 Go 語言中使用sync包中的RWMutex類型。
當goroutine對資源加讀鎖,其他goroutine也可以讀該資源,但不能對該資源做寫操作;當goroutine對資源加寫鎖,其他goroutine不能夠對該資源進行讀和寫操作。
`sync.RWMutex` 提供了以下5個方法。
| 方法名 | 功能 |
| :-: | :-: |
| func (rw *RWMutex) Lock() | 獲取寫鎖 |
| func (rw *RWMutex) Unlock() | 釋放寫鎖 |
| func (rw *RWMutex) RLock() | 獲取讀鎖 |
| func (rw *RWMutex) RUnlock() | 釋放讀鎖 |
| func (rw *RWMutex) RLocker() Locker | 返回一個實現Locker接口的讀寫鎖 |
寫個互斥鎖和讀寫互斥鎖的耗時對比。
```go
package main
import (
"fmt"
"sync"
"time"
)
var (
total int
swg sync.WaitGroup
sm sync.Mutex
srwm sync.RWMutex
)
func getTotalMutex() {
defer swg.Done()
// 假設讀操作需要 1 毫秒
sm.Lock()
time.Sleep(time.Millisecond)
fmt.Sprintln(total)
sm.Unlock()
}
func setTotalMutex() {
defer swg.Done()
// 假設寫操作需要 3 毫秒
sm.Lock()
time.Sleep(time.Millisecond * 3)
total++
sm.Unlock()
}
func getTotalRWMutex() {
defer swg.Done()
// 假設讀操作需要 1 毫秒
srwm.RLock()
time.Sleep(time.Millisecond)
fmt.Sprintln(total)
srwm.RUnlock()
}
func setTotalRWMutex() {
defer swg.Done()
// 假設寫操作需要 3 毫秒
srwm.Lock()
time.Sleep(time.Millisecond * 3)
total++
srwm.Unlock()
}
func main() {
// Mutex 鎖執行花費所用的時間
t1 := time.Now()
for i := 0; i < 50; i++ {
swg.Add(1)
go func() {
setTotalMutex()
}()
}
for i := 0; i < 500; i++ {
swg.Add(1)
go func() {
getTotalMutex()
}()
}
swg.Wait()
fmt.Printf("Mutex -> total: %v, cost: %v\n", total, time.Since(t1))
// RWMutex 鎖執行花費所用的時間
t2 := time.Now()
for i := 0; i < 50; i++ {
swg.Add(1)
go func() {
setTotalRWMutex()
}()
}
for i := 0; i < 500; i++ {
swg.Add(1)
go func() {
getTotalRWMutex()
}()
}
swg.Wait()
fmt.Printf("RWMutex -> total: %v, cost: %v\n", total, time.Since(t2))
}
// 運行結果
// Mutex -> total: 50, cost: 2.628357908s
// RWMutex -> total: 100, cost: 248.810775ms
```
## sync.WaitGroup(等待鎖)
在代碼中生硬的使用 `time.Sleep` 肯定是不合適的,Go語言中可以使用 `sync.WaitGroup` 來實現并發任務的同步。`sync.WaitGroup` 有以下幾個方法:
| 方法名 | 功能 |
| :-: | :-: |
| func (wg * WaitGroup) Add(delta int) | 計數器+delta |
| (wg *WaitGroup) Done() | 計數器-1 |
| (wg *WaitGroup) Wait() | 阻塞直到計數器變為0 |
`sync.WaitGroup` 內部維護著一個計數器,計數器的值可以增加和減少。例如當我們啟動了 N 個并發任務時,就將計數器值增加N。每個任務完成時通過調用 Done 方法將計數器減1。通過調用 Wait 來等待并發任務執行完,當計數器值為 0 時,表示所有并發任務已經完成。
```go
var swg sync.WaitGroup
func demo(i int) {
defer swg.Done()
fmt.Printf("Hello demo%d\n", i)
}
func main(){
fmt.Println("hello main begin~")
for i := 1; i <= 5; i++ {
swg.Add(1)
go demo(i)
}
swg.Wait()
fmt.Println("hello main end~")
}
// 運行結果
// hello main begin~
// Hello demo5
// Hello demo2
// Hello demo3
// Hello demo4
// Hello demo1
// hello main end~
```
## sync.Once
Once 常常用來初始化單例資源,或者并發訪問只需初始化一次的共享資源
`sync.Once` 主要用于以下場景:
- 單例模式:確保全局只有一個實例對象,避免重復創建資源。
- 延遲初始化:在程序運行過程中需要用到某個資源時,通過 sync.Once 動態地初始化該資源。
- 只執行一次的操作:例如只需要執行一次的配置加載、數據清理等操作。
示例代碼如下
```go
var (
swg sync.WaitGroup
once sync.Once
)
func main() {
swg.Add(1)
for i := 0; i < 155555; i++ {
go once.Do(func() {
defer swg.Done()
fmt.Println(i)
})
}
swg.Wait()
}
// 運行結果
// i: 137
```
>[info] 注意:`sync.WaitGroup` 只能 `Add()` 函數,無論并發有多少次,都只能為1。不然就會出現 `fatal error: all goroutines are asleep - deadlock!` 報錯
## sync.map
golang默認的map不是線程安全的,并發寫操作map時會panic `fatal error: concurrent map writes`。sync包提供了并發安全map的功能。采用了空間換時間的方法。在讀多寫少,寫入后不需要頻繁更新的場景比較適合使用。
sync.Map 內置常用幾種方法
| 方法名 | 功能 |
| :-: | :-: |
| func (m *Map) Store(key, value interface{}) | 存儲key-value數據 |
| func (m *Map) Load(key interface{}) (value interface{}, ok bool) | 查詢key對應的value |
| func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) | 查詢或存儲key對應的value |
| func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) | 查詢并刪除key |
| func (m *Map) Delete(key interface{}) | 刪除key |
| func (m *Map) Range(f func(key, value interface{}) bool) | 對map中的每個key-value依次調用f |
下面的代碼示例演示了并發讀寫sync.Map
```go
func mian() {
m := sync.Map{}
for i := 0; i < 5; i++ {
swg.Add(1)
go func(i int) {
defer swg.Done()
// 新增、修改
m.Store(fmt.Sprintf("name%d", i), i)
}(i)
}
swg.Wait()
// 查詢
key := "name0"
// value, ok := m.Load(key)
if value, ok := m.Load(key); ok {
fmt.Printf("value: %v\n", value)
} else {
fmt.Printf("%v is not exist", key)
}
// 刪除
key = "name4"
value, ok := m.LoadAndDelete(key)
if ok {
fmt.Printf("delete %v: %v\n", key, value)
} else {
fmt.Printf("%v is not exist\n", key)
}
// 遍歷變量m的key-value
m.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true
})
}
// 運行結果
// value: 0
// delete name4: 4
// name2 2
// name3 3
// name0 0
// name1 1
```
- Golang簡介
- 開發環境
- Golang安裝
- 編輯器及快捷鍵
- vscode插件
- 第一個程序
- 基礎數據類型
- 變量及匿名變量
- 常量與iota
- 整型與浮點型
- 復數與布爾值
- 字符串
- 運算符
- 算術運算符
- 關系運算符
- 邏輯運算符
- 位運算符
- 賦值運算符
- 流程控制語句
- 獲取用戶輸入
- if分支語句
- for循環語句
- switch語句
- break_continue_goto語法
- 高階數據類型
- pointer指針
- array數組
- slice切片
- slice切片擴展
- map映射
- 函數
- 函數定義和調用
- 函數參數
- 函數返回值
- 作用域
- 函數形參傳遞
- 匿名函數
- 高階函數
- 閉包
- defer語句
- 內置函數
- fmt
- strconv
- strings
- time
- os
- io
- 文件操作
- 編碼
- 字符與字節
- 字符串
- 讀寫文件
- 結構體
- 類型別名和自定義類型
- 結構體聲明
- 結構體實例化
- 模擬構造函數
- 方法接收器
- 匿名字段
- 嵌套與繼承
- 序列化
- 接口
- 接口類型
- 值接收者和指針接收者
- 類型與接口對應關系
- 空接口
- 接口值
- 類型斷言
- 并發編程
- 基本概念
- goroutine
- channel
- select
- 并發安全
- 練習題
- 第三方庫
- Survey
- cobra