「快學 Go 語言」第 13 課——併發與安全


「快學 Go 語言」第 13 課——併發與安全


上一節《 》我們提到併發編程不同的協程共享數據的方式除了通道之外還有就是共享變量。雖然 Go 語言官方推薦使用通道的方式來共享數據,但是通過變量來共享才是基礎,因為通道在底層也是通過共享變量的方式來實現的。通道的內部數據結構包含一個數組,對通道的讀寫就是對內部數組的讀寫。

在併發環境下共享讀寫變量必須要使用鎖來控制數據結構的安全,Go 語言內置了 sync 包,裡面包含了我們平時需要經常使用的互斥鎖對象 sync.Mutex。Go 語言內置的字典不是線程安全的,所以下面我們嘗試使用互斥鎖對象來保護字典,讓它變成線程安全的字典。


「快學 Go 語言」第 13 課——併發與安全


線程不安全的字典

Go 語言內置了數據結構「競態檢查」工具來幫我們檢查程序中是否存在線程不安全的代碼。當我們在運行代碼時,打開 -run 開關,程序就會在內置的通用數據結構中進行埋點檢查。競態檢查工具在 Go 1.1 版本中引入,該功能幫助 Go 語言「元團隊」找出了 Go 語言標準庫中幾十個存在線程安全隱患的 bug,這是一個非常了不起的功能。同時這也說明了即使是猿界的神仙,寫出來的代碼也避免不了有 bug。下面我們來嘗試一下

<code>package main

import "fmt"

func write(d map[string]int) {
\td["fruit"] = 2
}

func read(d map[string]int) {
\tfmt.Println(d["fruit"])
}

func main() {
\td := map[string]int{}
\tgo read(d)
\twrite(d)
}
複製代碼/<code>

上面的代碼明顯存在安全隱患,運行下面的競態檢查指令觀察輸出結果

<code>$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c420090180 by goroutine 6:
  runtime.mapaccess1_faststr()
/usr/local/Cellar/go/1.10.3/libexec/src/runtime/hashmap_fast.go:172 +0x0
  main.read()
      ~/go/src/github.com/pyloque/practice/main.go:10 +0x5d

Previous write at 0x00c420090180 by main goroutine:
  runtime.mapassign_faststr()
/usr/local/Cellar/go/1.10.3/libexec/src/runtime/hashmap_fast.go:694 +0x0
  main.main()
      ~/go/src/github.com/pyloque/practice/main.go:6 +0x88

Goroutine 6 (running) created at:
  main.main()
      ~/go/src/github.com/pyloque/practice/main.go:15 +0x59
==================
==================
WARNING: DATA RACE
Read at 0x00c4200927d8 by goroutine 6:
  main.read()
      ~/go/src/github.com/pyloque/practice/main.go:10 +0x70

Previous write at 0x00c4200927d8 by main goroutine:
  main.main()
      ~/go/src/github.com/pyloque/practice/main.go:6 +0x9b

Goroutine 6 (running) created at:
  main.main()
      ~/go/src/github.com/pyloque/practice/main.go:15 +0x59
==================
2
Found 2 data race(s)
複製代碼/<code>

競態檢查工具是基於運行時代碼檢查,而不是通過代碼靜態分析來完成的。這意味著那些沒有機會運行到的代碼邏輯中如果存在安全隱患,它是檢查不出來的。


「快學 Go 語言」第 13 課——併發與安全


線程安全的字典

讓字典變的線程安全,就需要對字典的所有讀寫操作都使用互斥鎖保護起來。

<code>package main

import "fmt"
import "sync"

type SafeDict struct {
\tdata  map[string]int
\tmutex *sync.Mutex
}


func NewSafeDict(data map[string]int) *SafeDict {
\treturn &SafeDict{
\t\tdata:  data,
\t\tmutex: &sync.Mutex{},
\t}
}

func (d *SafeDict) Len() int {
\td.mutex.Lock()
\tdefer d.mutex.Unlock()
\treturn len(d.data)
}

func (d *SafeDict) Put(key string, value int) (int, bool) {
\td.mutex.Lock()
\tdefer d.mutex.Unlock()
\told_value, ok := d.data[key]
\td.data[key] = value
\treturn old_value, ok
}

func (d *SafeDict) Get(key string) (int, bool) {
\td.mutex.Lock()
\tdefer d.mutex.Unlock()
\told_value, ok := d.data[key]
\treturn old_value, ok
}

func (d *SafeDict) Delete(key string) (int, bool) {
\td.mutex.Lock()
\tdefer d.mutex.Unlock()
\told_value, ok := d.data[key]
\tif ok {
\t\tdelete(d.data, key)
\t}
\treturn old_value, ok
}

func write(d *SafeDict) {
\td.Put("banana", 5)
}

func read(d *SafeDict) {
\tfmt.Println(d.Get("banana"))
}

func main() {
\td := NewSafeDict(map[string]int{
\t\t"apple": 2,

\t\t"pear":  3,
\t})
\tgo read(d)
\twrite(d)
}
複製代碼/<code>

嘗試使用競態檢查工具運行上面的代碼,會發現沒有了剛才一連串的警告輸出,說明 Get 和 Put 方法已經做到了協程安全,但是還不能說明 Delete() 方法是否安全,因為它根本沒有機會得到運行。

在上面的代碼中我們再次看到了 defer 語句的應用場景 —— 釋放鎖。defer 語句總是要推遲到函數尾部運行,所以如果函數邏輯運行時間比較長,這會導致鎖持有的時間較長,這時使用 defer 語句來釋放鎖未必是一個好注意。

避免鎖複製

上面的代碼中還有一個需要特別注意的地方是 sync.Mutex 是一個結構體對象,這個對象在使用的過程中要避免被複制 —— 淺拷貝。複製會導致鎖被「分裂」了,也就起不到保護的作用。所以在平時的使用中要儘量使用它的指針類型。讀者可以嘗試將上面的類型換成非指針類型,然後運行一下競態檢查工具,會看到警告信息再次佈滿整個屏幕。鎖複製存在於結構體變量的賦值、函數參數傳遞、方法參數傳遞中,都需要注意。

使用匿名鎖字段

在結構體章節,我們知道外部結構體可以自動繼承匿名內部結構體的所有方法。如果將上面的 SafeDict 結構體進行改造,將鎖字段匿名,就可以稍微簡化一下代碼。

<code>package main

import "fmt"
import "sync"

type SafeDict struct {
\tdata  map[string]int
\t*sync.Mutex
}

func NewSafeDict(data map[string]int) *SafeDict {
\treturn &SafeDict{data, &sync.Mutex{}}
}

func (d *SafeDict) Len() int {
\td.Lock()
\tdefer d.Unlock()
\treturn len(d.data)
}

func (d *SafeDict) Put(key string, value int) (int, bool) {
\td.Lock()
\tdefer d.Unlock()
\told_value, ok := d.data[key]
\td.data[key] = value
\treturn old_value, ok
}

func (d *SafeDict) Get(key string) (int, bool) {
\td.Lock()
\tdefer d.Unlock()
\told_value, ok := d.data[key]
\treturn old_value, ok
}

func (d *SafeDict) Delete(key string) (int, bool) {
\td.Lock()
\tdefer d.Unlock()
\told_value, ok := d.data[key]

\tif ok {
\t\tdelete(d.data, key)
\t}
\treturn old_value, ok
}

func write(d *SafeDict) {
\td.Put("banana", 5)
}

func read(d *SafeDict) {
\tfmt.Println(d.Get("banana"))
}

func main() {
\td := NewSafeDict(map[string]int{
\t\t"apple": 2,
\t\t"pear":  3,
\t})
\tgo read(d)
\twrite(d)
}
複製代碼/<code>

使用讀寫鎖

日常應用中,大多數併發數據結構都是讀多寫少的,對於讀多寫少的場合,可以將互斥鎖換成讀寫鎖,可以有效提升性能。sync 包也提供了讀寫鎖對象 RWMutex,不同於互斥鎖只有兩個常用方法 Lock() 和 Unlock(),讀寫鎖提供了四個常用方法,分別是寫加鎖 Lock()、寫釋放鎖 Unlock()、讀加鎖 RLock() 和讀釋放鎖 RUnlock()。寫鎖是排他鎖,加寫鎖時會阻塞其它協程再加讀鎖和寫鎖,讀鎖是共享鎖,加讀鎖還可以允許其它協程再加讀鎖,但是會阻塞加寫鎖。

讀寫鎖在寫併發高的情況下性能退化為普通的互斥鎖。下面我們將代碼中 SafeDict 的互斥鎖改造成讀寫鎖。

<code>package main

import "fmt"
import "sync"

type SafeDict struct {
\tdata  map[string]int
\t*sync.RWMutex
}

func NewSafeDict(data map[string]int) *SafeDict {
\treturn &SafeDict{data, &sync.RWMutex{}}
}

func (d *SafeDict) Len() int {
\td.RLock()
\tdefer d.RUnlock()
\treturn len(d.data)
}

func (d *SafeDict) Put(key string, value int) (int, bool) {
\td.Lock()
\tdefer d.Unlock()
\told_value, ok := d.data[key]
\td.data[key] = value
\treturn old_value, ok
}

func (d *SafeDict) Get(key string) (int, bool) {
\td.RLock()
\tdefer d.RUnlock()
\told_value, ok := d.data[key]
\treturn old_value, ok
}

func (d *SafeDict) Delete(key string) (int, bool) {
\td.Lock()
\tdefer d.Unlock()
\told_value, ok := d.data[key]
\tif ok {
\t\tdelete(d.data, key)
\t}
\treturn old_value, ok
}

func write(d *SafeDict) {
\td.Put("banana", 5)
}

func read(d *SafeDict) {
\tfmt.Println(d.Get("banana"))
}

func main() {
\td := NewSafeDict(map[string]int{
\t\t"apple": 2,
\t\t"pear":  3,
\t})
\tgo read(d)
\twrite(d)
}
複製代碼/<code>

下一節我們要開始嘗試 Go 語言學習的難點之一 —— 反射。


分享到:


相關文章: