當 Go struct 趕上 Mutex

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

一段代碼

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
}

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

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

若是你看出來問題在哪裏的話, 那我再悄悄告訴你, 這段代碼是 github 某 3k star Go 框架的底層核心代碼, 那你是否是就以爲這個話題開始有意思了 ?golang

先說結論

上面那段代碼的問題是 sync.RWMutex 引發的. 若是你看過有關 sync 相關類型的介紹或者相關源碼時, 在 sync 包裏面的全部類型都有句這樣的註釋: must not be copied after first use, 可能不少人卻並不知道這句話有什麼做用, 頂多看到相關介紹時還記得 sync 相關類型的變量不能複製, 可能真正使用 Mutex, WaitGroup, Cond時, 早把這個註釋忘的一乾二淨. app

究其緣由, 我以爲有下面兩點緣由:框架

  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 變量隨着值傳遞對外擴散時, 這個時候就會變得不可控了, 就須要特別當心了.ui

  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:this

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....google

例子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{}

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

歡迎關注個人公衆號: HHFCodeRv, 關注個人最新動態

相關文章
相關標籤/搜索