《快學 Go 語言》第 13 課 —— 併發與安全

上一節咱們提到併發編程不一樣的協程共享數據的方式除了通道以外還有就是共享變量。雖然 Go 語言官方推薦使用通道的方式來共享數據,可是經過變量來共享纔是基礎,由於通道在底層也是經過共享變量的方式來實現的。通道的內部數據結構包含一個數組,對通道的讀寫就是對內部數組的讀寫。git

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

線程不安全的字典

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

package main

import "fmt"

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

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

func main() {
	d := map[string]int{}
	go read(d)
	write(d)
}
複製代碼

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

$ 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)
複製代碼

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

線程安全的字典

讓字典變的線程安全,就須要對字典的全部讀寫操做都使用互斥鎖保護起來。數據結構

package main

import "fmt"
import "sync"

type SafeDict struct {
	data  map[string]int
	mutex *sync.Mutex
}

func NewSafeDict(data map[string]int) *SafeDict {
	return &SafeDict{
		data:  data,
		mutex: &sync.Mutex{},
	}
}

func (d *SafeDict) Len() int {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	return len(d.data)
}

func (d *SafeDict) Put(key string, value int) (int, bool) {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	old_value, ok := d.data[key]
	d.data[key] = value
	return old_value, ok
}

func (d *SafeDict) Get(key string) (int, bool) {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	old_value, ok := d.data[key]
	return old_value, ok
}

func (d *SafeDict) Delete(key string) (int, bool) {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	old_value, ok := d.data[key]
	if ok {
		delete(d.data, key)
	}
	return old_value, ok
}

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

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

func main() {
	d := NewSafeDict(map[string]int{
		"apple": 2,
		"pear"3,
	})
	go read(d)
	write(d)
}
複製代碼

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

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

避免鎖複製

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

使用匿名鎖字段

在結構體章節,咱們知道外部結構體能夠自動繼承匿名內部結構體的全部方法。若是將上面的 SafeDict 結構體進行改造,將鎖字段匿名,就能夠稍微簡化一下代碼。工具

package main

import "fmt"
import "sync"

type SafeDict struct {
	data  map[string]int
	*sync.Mutex
}

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

func (d *SafeDict) Len() int {
	d.Lock()
	defer d.Unlock()
	return len(d.data)
}

func (d *SafeDict) Put(key string, value int) (int, bool) {
	d.Lock()
	defer d.Unlock()
	old_value, ok := d.data[key]
	d.data[key] = value
	return old_value, ok
}

func (d *SafeDict) Get(key string) (int, bool) {
	d.Lock()
	defer d.Unlock()
	old_value, ok := d.data[key]
	return old_value, ok
}

func (d *SafeDict) Delete(key string) (int, bool) {
	d.Lock()
	defer d.Unlock()
	old_value, ok := d.data[key]
	if ok {
		delete(d.data, key)
	}
	return old_value, ok
}

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

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

func main() {
	d := NewSafeDict(map[string]int{
		"apple": 2,
		"pear"3,
	})
	go read(d)
	write(d)
}
複製代碼

使用讀寫鎖

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

讀寫鎖在寫併發高的狀況下性能退化爲普通的互斥鎖。下面咱們將代碼中 SafeDict 的互斥鎖改形成讀寫鎖。

package main

import "fmt"
import "sync"

type SafeDict struct {
	data  map[string]int
	*sync.RWMutex
}

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

func (d *SafeDict) Len() int {
	d.RLock()
	defer d.RUnlock()
	return len(d.data)
}

func (d *SafeDict) Put(key string, value int) (int, bool) {
	d.Lock()
	defer d.Unlock()
	old_value, ok := d.data[key]
	d.data[key] = value
	return old_value, ok
}

func (d *SafeDict) Get(key string) (int, bool) {
	d.RLock()
	defer d.RUnlock()
	old_value, ok := d.data[key]
	return old_value, ok
}

func (d *SafeDict) Delete(key string) (int, bool) {
	d.Lock()
	defer d.Unlock()
	old_value, ok := d.data[key]
	if ok {
		delete(d.data, key)
	}
	return old_value, ok
}

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

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

func main() {
	d := NewSafeDict(map[string]int{
		"apple": 2,
		"pear"3,
	})
	go read(d)
	write(d)
}
複製代碼

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

閱讀《快學 Go 語言》更多章節,長按圖片識別二維碼關注公衆號「碼洞」

相關文章
相關標籤/搜索