當 Go struct 遇上 Mutex


struct 是我們寫 Go 必然會用到的關鍵字, 不過當 struct 遇上一些比較特殊類型的時候, 你注意過你的程序是否正常嗎 ?

一段代碼

type URL struct {
	Ip       string
	Port     string
	mux   	 sync.RWMutex
	params    url.Values
}

func (c *URL) Clone() URL {
	newUrl := URL{}
	newUrl.Ip = c.Ip
	newUrl.params = url.Values{}
	return newUrl
}

這段代碼你能看出來問題所在嗎 ?

A: 程序正常
B: 編譯失敗
C: panic
D: 有可能發生 data race
E: 有可能發生死鎖

如果你看出來問題在哪里的話, 那我再悄悄告訴你, 這段代碼是 github 某 3k star Go 框架的底層核心代碼, 那你是不是就覺得這個話題開始有意思了 ?

先說結論

上面那段代碼的問題是 sync.RWMutex 引起的. 如果你看過有關 sync 相關類型的介紹或者相關源碼時, 在 sync 包里面的所有類型都有句這樣的注釋: must not be copied after first use, 可能很多人卻並不知道這句話有什么作用, 頂多看到相關介紹時還記得 sync 相關類型的變量不能復制, 可能真正使用 Mutex, WaitGroup, Cond時, 早把這個注釋忘的一干二凈.

究其原因, 我覺得有下面兩點原因:

  1. 不明白什么叫 sync 類型變量復制
  2. sync 類型的變量復制了會出現怎樣的結果

下面的例子都以 Mutex 來舉例

  1. 最容易看出來的情形
func main() {
	var amux sync.Mutex
	b := amux
	b.Lock()
	b.Unlock()
}

其實這種情況一般情況下, 沒人這么用. 問題不大, 略過

  1. 嵌套在 struct 里面, struct 變量間的互相賦值
type URL struct {
	Ip       string
	Port     string
	mux   	 sync.RWMutex
	params    url.Values
}

func main() {
	var url1 URL
	url2 := url1
}

當 struct 嵌套 不可復制 類型時, 就需要開始小心了. 當 struct 嵌套層次過深或者 struct 變量隨着值傳遞對外擴散時, 這個時候就會變得不可控了, 就需要特別小心了.

  1. struct 類型變量的值傳遞作為返回值
type URL struct {
	Ip       string
	mux   	 sync.RWMutex
}

func (c *URL) Clone() URL {
	newUrl := URL{}
	newUrl.Ip = c.Ip
	return newUrl
}
  1. struct 類型變量的值傳遞作為 receiver
type URL struct {
	Ip       string
	mux   	 sync.RWMutex
}

func (c URL) String() string {
	c.paramsLock.Lock()
	defer c.paramsLock.Unlock()
	buf.WriteString(c.params.Encode())
	return buf.String()
}

復制后出現的結果

例子1:

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup
var age int

type Person struct {
	mux sync.Mutex
}

func (p Person) AddAge() {
	defer wg.Done()
	p.mux.Lock()
	age++
	defer p.mux.Unlock()

}

func main() {
	p1 := Person{
		mux: sync.Mutex{},
	}
	wg.Add(100)
	for i := 0; i < 100; i++ {
		go p1.AddAge()
	}
	wg.Wait()
	fmt.Println(age)
}

結果: 結果有可能是 100, 也有可能是99....

例子2:

package main

import (
	"fmt"
	"sync"
)

type Person struct {
	mux sync.Mutex
}

func Reduce(p Person) {
	fmt.Println("step...", )
	p.mux.Lock()
	fmt.Println(p)
	defer p.mux.Unlock()
	fmt.Println("over...")
}

func main() {
	var p Person
	p.mux.Lock()
	go Reduce(p)
	p.mux.Unlock()
	fmt.Println(111)
	for {
	}
}

結果: Reduce 協程會死鎖.

看到這里我們就能發現, 當 struct 嵌套了 Mutex, 如果以值傳遞的方式使用時, 有可能造成程序死鎖, 有可能需要互斥的變量並不能達到互斥.

所以不管是單獨使用 不能復制 類型的變量, 還是嵌套在 struct 里面都不能值傳遞的方式使用.

不能復制原因

以 Mutex 為例,

type Mutex struct {
	state int32
	sema  uint32
}

我們使用 Mutex 是為了不同 goroutine 之間共享某個變量, 所以需要讓這個變量做到能夠互斥, 不然該變量就會被互相被覆蓋. Mutex 底層是由 state sema 控制的, 當 Mutex 變量被復制時, Mutex 的 state, sema 當時的狀態也被復制走了, 但是由於不同 goroutine 之間的 Mutex 已經不是同一個變量了, 這樣就會造成要么某個 goroutine 死鎖或者不同 goroutine 共享的變量達不到互斥

struct 如何與 不可復制 的類型一塊使用 ?

由上面可以看到不只是 sync 相關類型變量自身不能被復制,而且 sturct 嵌套 不可復制 類型變量時, 同樣也不能被復制. 但是如果我將嵌套的不可復制變量改成指針類型變量呢, 是不是就解決了不能復制的問題 ?

type URL struct {
	Ip       string
	mux   	 *sync.RWMutex
}

這樣確實解決了上述的不能復制問題. 但也引出了另外一個問題. 眾所周知 Go 沒有構造函數, 這就導致我們使用 URL 的時候都需要先去初始化 RWMutex, 不然就會造成同樣很嚴重的空指針問題, 這個問題同樣很棘手,也許哪個位置就忘了初始化這個 RWMutex.

根據 google groups 的討論 How to copy a struct which contains a mutex?, 以及我查看了Kubernets 的相關源碼(這里只是一個例子, 里面還有很多), 發現大家的觀點基本上都是一致的, 都不會去選用 struct 去嵌套指針類型的變量, 由此不建議 struct 去嵌套 不可復制的 的指針類型變量. 最重要的原因: 沒有一個工具能去准確的檢測空指針.

所以一般情況下, 當 struct 嵌套了 不可復制 類型的變量時, 都需要傳遞的是 struct 類型變量的指針.

如何防止復制了不該復制的變量呢?

由於 Go 並不提供重載的功能, 所以並不能做到去重載 struct 的相關的被復制的方法. 但是 Go 的槽點就來了, Go 本身還不提供不能被復制的相關的編譯強約束. 這樣就有可能導致出現不能被復制的類型被復制過后蒙混過關. 那我們需要怎么做呢 ?

Go 提供了另外一個工具 go vet 來做補充, 用這個工具是能檢測出來不可復制的類型是否被復制過.

func main() {
	var amux sync.Mutex
	b := amux
	b.Lock()
	b.Unlock()
}
$ go vet main.go
# command-line-arguments
./main.go:7:7: assignment copies lock value to b: sync.Mutex

我們怎么把 go vet 與 日常開發結合起來呢?

  1. 目前的 Goland, Vscode 都會集成 go vet 的相關功能, 如果你強迫症比較嚴重的話, 你就能發現有相關提示.
  2. 把 go vet 與 CI 流程結合起來, 其實更推薦使用 golangci-lint 這個 lint 工具來做 CI

Go 還提供一段 noCopy 的代碼, 當你的 struct 有不能被復制的需求的時候, 可以加入這段代碼

type noCopy struct{}

// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

這段代碼依然是給 go vet 來使用的.

說到這里, 禁止復制不能被復制的變量, 這個明明能在 編譯期 就杜絕的事情, 為啥非要搞出來工具來做這個事情呢? 有點想不通.

不可復制的類型有哪些?

Go 提供的不可復制的類型基本上就是 sync 包內的所有類型: atomic.Value, sync.Mutex, sync.Cond, sync.RWMutex, sync.Map, sync.Pool, sync.WaitGroup.

這些內置的不可被復制的類型當被復制時配合 go vet是能夠發現的. 但是下面這種場景你是否遇見過?

package main

import "fmt"

type Books struct {
	someImportantData []int
}

func DoSomething(otherBook Books) Books {
	newBook := otherBook
	// do something
	for k := range newBook.someImportantData {
		newBook.someImportantData[k]++ // just like this
	}
	return otherBook
}

func main() {
	oldBook := Books{
		someImportantData: make([]int, 0, 100),
	}

	oldBook.someImportantData = append(oldBook.someImportantData, 1, 2, 3)
	fmt.Println("before DoSomething, old book:", oldBook.someImportantData)
	DoSomething(oldBook)
	fmt.Println("after DoSomething, old book:", oldBook.someImportantData)
	// 使用oldBook.someImportantData 繼續做某些事情
}

結果:

before DoSomething, old book: [1 2 3]
after DoSomething, old book: [2 3 4]

這個場景其實我們可能不經意間就會遇到. oldBook 是我們要操作的數據, 但是通過 DoSomething` 后, oldBook.someImportantData 的值可能就被改掉了, 這可能並不是我們所期待的. 由於 DoSomething 是通過復制傳遞的, 可能我們並不能很敏感關注到這個點, 導致程序繼續往下走邏輯可能就錯了. 我們是不是可以設置 Books 為不可復制呢 ? 這樣可以讓 go vet 幫助我們發現這些問題

最后的最后

你是否這樣初始化過 WaitGroup ?

wg := sync.WaitGroup{}

這個算不算是被復制了呢, 歡迎留言討論.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM