Go語言的協(xié)程與并發(fā)

1 Channel

channel 是Go語言在語言級(jí)別提供的 goroutine 間的通信方式猎唁。我們可以使用 channel 在兩個(gè)或多個(gè) goroutine 之間傳遞消息。
channel可以作為一個(gè)先入先出(FIFO)的隊(duì)列顷蟆,接收的數(shù)據(jù)和發(fā)送的數(shù)據(jù)的順序是一致的诫隅。
channel 是類型相關(guān)的,也就是說帐偎,一個(gè) channel 只能傳遞一種類型的值逐纬,這個(gè)類型需要在聲明 channel 時(shí)指定。如果對(duì) Unix 管道有所了解的話削樊,就不難理解 channel豁生,可以將其認(rèn)為是一種類型安全的管道。
定義一個(gè) channel 時(shí)漫贞,也需要定義發(fā)送到 channel 的值的類型甸箱,注意,必須使用 make 創(chuàng)建 channel迅脐,代碼如下所示:

ci := make(chan int)
cs := make(chan string)
//可以指定容量摇肌,默認(rèn)為0
cf := make(chan interface{}, 100)

channel有三種類型,默認(rèn)是雙向的:

chan T          // 可以接收和發(fā)送類型為 T 的數(shù)據(jù)
chan<- float64  // 只可以用來發(fā)送 float64 類型的數(shù)據(jù)
<-chan int      // 只可以用來接收 int 類型的數(shù)據(jù)

chan<- chan int    // 等價(jià) chan<- (chan int)
chan<- <-chan int  // 等價(jià) chan<- (<-chan int)
<-chan <-chan int  // 等價(jià) <-chan (<-chan int)

chan (<-chan int)

發(fā)送和接收消息:

//發(fā)送消息
ch <- 3
//接收消息
v, ok := <-ch
//for循環(huán)也可以處理channel
for i := range c {
    fmt.Println(i)
}

1.1 blocking channel

image.png

1.2 buffered channel

Go語言中有緩沖的通道(buffered channel)是一種在被接收前能存儲(chǔ)一個(gè)或者多個(gè)值的通道仪际。這種類型的通道并不強(qiáng)制要求 goroutine 之間必須同時(shí)完成發(fā)送和接收围小。通道會(huì)阻塞發(fā)送和接收動(dòng)作的條件也會(huì)不同。只有在通道中沒有要接收的值時(shí)树碱,接收動(dòng)作才會(huì)阻塞肯适。只有在通道沒有可用緩沖區(qū)容納被發(fā)送的值時(shí),發(fā)送動(dòng)作才會(huì)阻塞成榜。

這導(dǎo)致有緩沖的通道和無緩沖的通道之間的一個(gè)很大的不同:無緩沖的通道保證進(jìn)行發(fā)送和接收的 goroutine 會(huì)在同一時(shí)間進(jìn)行數(shù)據(jù)交換框舔;有緩沖的通道沒有這種保證。

緩沖區(qū)的實(shí)現(xiàn)可以參考channel內(nèi)部組成結(jié)構(gòu):


image.png

1.3 select關(guān)鍵字

select處理邏輯

  • 如果有同時(shí)多個(gè)case去處理,比如同時(shí)有多個(gè)channel可以接收數(shù)據(jù),那么Go會(huì)偽隨機(jī)的選擇一個(gè)case處理(pseudo-random)刘绣。
  • 如果沒有case需要處理樱溉,則會(huì)選擇default去處理,如果default case存在的情況下纬凤。
  • 如果沒有default case福贞,則select語句會(huì)阻塞,直到某個(gè)case需要處理停士。
import "fmt"
func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}
func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

select有很重要的一個(gè)應(yīng)用就是超時(shí)處理挖帘。 因?yàn)樯厦嫖覀兲岬剑绻麤]有case需要處理恋技,select語句就會(huì)一直阻塞著拇舀。這時(shí)候我們可能就需要一個(gè)超時(shí)操作,用來處理超時(shí)的情況蜻底。
下面這個(gè)例子我們會(huì)在2秒后往channel c1中發(fā)送一個(gè)數(shù)據(jù)骄崩,但是select設(shè)置為1秒超時(shí),因此我們會(huì)打印出timeout 1,而不是result 1。

import "time"
import "fmt"
func main() {
    c1 := make(chan string, 1)
    go func() {
        time.Sleep(time.Second * 2)
        c1 <- "result 1"
    }()
    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(time.Second * 1):
        fmt.Println("timeout 1")
    }
}

1.4 close函數(shù)

內(nèi)建的close方法可以用來關(guān)閉channel薄辅。
總結(jié)一下channel關(guān)閉后sender的receiver操作要拂。
如果channel c已經(jīng)被關(guān)閉,繼續(xù)往它發(fā)送數(shù)據(jù)會(huì)導(dǎo)致panic: send on closed channel。

import "time"
func main() {
    go func() {
        time.Sleep(time.Hour)
    }()
    c := make(chan int, 10)
    c <- 1
    c <- 2
    close(c)
    c <- 3
}

但是從這個(gè)關(guān)閉的channel中不但可以讀取出已發(fā)送的數(shù)據(jù)长搀,還可以不斷的讀取零值:

c := make(chan int, 10)
c <- 1
c <- 2
close(c)
fmt.Println(<-c) //1
fmt.Println(<-c) //2
fmt.Println(<-c) //0
fmt.Println(<-c) //0

但是如果通過range讀取宇弛,channel關(guān)閉后for循環(huán)會(huì)跳出:

c := make(chan int, 10)
c <- 1
c <- 2
close(c)
for i := range c {
    fmt.Println(i)
}

通過i, ok := <-c可以查看Channel的狀態(tài),判斷值是零值還是正常讀取的值源请。

c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false

1.5 用法

go社區(qū)給出的goroutine開發(fā)原則是:

不要通過共享內(nèi)存來通信枪芒,而應(yīng)該通過通信來共享內(nèi)存

channel的存在就是為了減少線程間共享內(nèi)存的操作,而使用通信的方式在實(shí)現(xiàn)線程間的同步和通信
下面設(shè)計(jì)了一個(gè)Benchmark測試谁尸,用來驗(yàn)證channel和傳統(tǒng)互斥鎖方式同步的性能差異:

package main

import (
    "errors"
    "sync"
    "testing"
)

type Game struct {
    mtx       sync.Mutex
    bestScore int
    scores    chan int
}

func NewGame() *Game {
    return &Game{
        mtx:       sync.Mutex{},
        bestScore: 0,
        scores:    make(chan int, 10),
    }
}

type Player struct {
    count int
}

func (p *Player) NextScore() (score int, err error) {
    p.count++
    if p.count < 1000000 {
        return p.count, nil
    } else {
        return 0, errors.New("")
    }
}

func (g *Game) run() {
    for score := range g.scores {
        if g.bestScore < score {
            g.bestScore = score
        }
    }
}

func (g *Game) HandlePlayerChannel() error {
    p := Player{}
    for {
        score, err := p.NextScore()
        if err != nil {
            return err
        }
        g.scores <- score
    }
}

func (g *Game) HandlePlayerMutex() error {
    p := Player{}
    for {
        score, err := p.NextScore()
        if err != nil {
            return err
        }
        g.mtx.Lock()
        if g.bestScore < score {
            g.bestScore = score
        }
        g.mtx.Unlock()
    }
}

func BenchmarkChannel(b *testing.B) {
    game := NewGame()
    go game.run()
    b.ResetTimer()
    for i := 0; i <= 200; i++ {
        go game.HandlePlayerChannel()
    }
    b.StopTimer()
}

func BenchmarkMutex(b *testing.B) {
    game := NewGame()
    b.ResetTimer()
    for i := 0; i <= 200; i++ {
        go game.HandlePlayerMutex()
    }
    b.StopTimer()
}
mutex運(yùn)行結(jié)果

buffer為0時(shí)channel

buffer為10時(shí)channel

buffer為100時(shí)channel

整體來看舅踪,兩者的速度相差不大

2 Runtime與Debug包

2.1 常用函數(shù)

runtime.GOMAXPROCS函數(shù)
通過runtime.GOMAXPROCS函數(shù),應(yīng)用程序何以在運(yùn)行期間設(shè)置運(yùn)行時(shí)系統(tǒng)中得P最大數(shù)量良蛮,但這會(huì)引起\color{red}{Stop the Word}抽碌。所以應(yīng)在應(yīng)用程序最早的調(diào)用。并且最好的設(shè)置P最大值的方法是在運(yùn)行Go程序之前設(shè)置好操作程序的環(huán)境變量GOMAXPROCS,而不是在程序中調(diào)用runtime.GOMAXPROCS函數(shù)决瞳。
最后記住货徙,無論我們傳遞給函數(shù)的整數(shù)值是什么值,運(yùn)行時(shí)系統(tǒng)的P最大值總會(huì)在1~256之間皮胡。
runtime.Goexit函數(shù)
runtime.Goexit函數(shù)被調(diào)用后痴颊,會(huì)立即使調(diào)用他的Groution的運(yùn)行被終止,但其他Goroutine并不會(huì)受到影響屡贺。runtime.Goexit函數(shù)在終止調(diào)用它的Goroutine的運(yùn)行之前會(huì)先執(zhí)行該Groution中還沒有執(zhí)行的defer語句蠢棱。
runtime.Gosched函數(shù)
runtime.Gosched函數(shù)的作用是暫停調(diào)用他的Goroutine的運(yùn)行锌杀,調(diào)用他的Goroutine會(huì)被重新置于Gorunnable狀態(tài),并被放入調(diào)度器可運(yùn)行G隊(duì)列中泻仙。
runtime.NumGoroutine函數(shù)
runtime.NumGoroutine函數(shù)在被調(diào)用后糕再,會(huì)返回系統(tǒng)中的處于特定狀態(tài)的Goroutine的數(shù)量。這里的特指是指Grunnable\Gruning\Gsyscall\Gwaition玉转。處于這些狀態(tài)的Groutine即被看做是活躍的或者說正在被調(diào)度突想。
注意:垃圾回收所在Groutine的狀態(tài)也處于這個(gè)范圍內(nèi)的話,也會(huì)被納入該計(jì)數(shù)器冤吨。
runtime.LockOSThread和runtime.UnlockOSThread函數(shù)
前者調(diào)用會(huì)使調(diào)用他的Goroutine與當(dāng)前運(yùn)行它的M一對(duì)一綁定蒿柳,并且直接由操作系統(tǒng)調(diào)度饶套,UnlockOSThread會(huì)解除這樣的鎖定漩蟆。
注意:

  1. 多次調(diào)用前者不會(huì)出現(xiàn)任何問題,但最后一次調(diào)用的記錄會(huì)被保留妓蛮,
  2. 即時(shí)之前沒有調(diào)用前者怠李,對(duì)后者的調(diào)用也不會(huì)產(chǎn)生任何副作用
  3. 綁定后G創(chuàng)建的子G不會(huì)被綁定
  4. time.Sleep等操作建議改為等效的syscall操作,否則這些操作效果可能達(dá)不到預(yù)期

runtime.SetFinalizer函數(shù)
類似于Java中的finalize函數(shù)蛤克,會(huì)在對(duì)象沒有引用且GC準(zhǔn)備回收該對(duì)象時(shí)被調(diào)用捺癞。

  1. 即使程序正常結(jié)束或者發(fā)生錯(cuò)誤, 但是在對(duì)象被 gc 選中并被回收之前构挤,SetFinalizer 都不會(huì)執(zhí)行髓介, 所以不要在SetFinalizer中執(zhí)行將內(nèi)存中的內(nèi)容flush到磁盤這種操作
  2. SetFinalizer 最大的問題是延長了對(duì)象生命周期。在第一次回收時(shí)執(zhí)行 Finalizer 函數(shù)筋现,且目標(biāo)對(duì)象重新變成可達(dá)狀態(tài)唐础,直到第二次才真正 “銷毀”。這對(duì)于有大量對(duì)象分配的高并發(fā)算法矾飞,可能會(huì)造成很大麻煩
  3. 指針構(gòu)成的 "循環(huán)引?" 加上 runtime.SetFinalizer 會(huì)導(dǎo)致內(nèi)存泄露

debug.SetMaxStack函數(shù)
debug.SetMaxStack函數(shù)的功能是約束單個(gè)Groutine所能申請(qǐng)的椧慌颍空間的最大尺寸。
debug.SetMaxThreads函數(shù)
debug.SetMaxThreads函數(shù)的功能是對(duì)go語言運(yùn)行時(shí)系統(tǒng)所使用的內(nèi)核線程的數(shù)量(確切的說是M的數(shù)量)進(jìn)行設(shè)置

2.2 使用案例

如果限定GOMAXPROCS的值為1洒沦,下面的測試用例運(yùn)行結(jié)果是豹绪?

func TestRuntime(t *testing.T) {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(20)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A: ", i)
            wg.Done()
        }()
    }
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println("B: ", i)
            wg.Done()
        }(i)
    }
    
    fmt.Printf("---main end loop---\n")
    wg.Wait()
    fmt.Printf("---  main exit  ---\n")
}

在Go 1.16中的運(yùn)行結(jié)果

=== RUN   TestRuntime
---main end loop---
B:  9
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
B:  0
B:  1
B:  2
B:  3
B:  4
B:  5
B:  6
B:  7
B:  8
---  main exit  ---
--- PASS: TestRuntime (0.00s)
PASS

在Go 1.4中的運(yùn)行結(jié)果

=== RUN   TestRuntime
---main end loop---
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
B:  0
B:  1
B:  2
B:  3
B:  4
B:  5
B:  6
B:  7
B:  8
B:  9
---  main exit  ---
--- PASS: TestRuntime (0.00s)
PASS

可以看到---main end loop---總是最先輸出,表明在1個(gè)操作系統(tǒng)線程的情況下申眼,只有main協(xié)程執(zhí)行到wg.Wait()阻塞等待時(shí)瞒津,其子協(xié)程才能被執(zhí)行,而子協(xié)程的執(zhí)行順序正好對(duì)應(yīng)于它們?nèi)腙?duì)列的順序括尸。

新版本中先打印9是因?yàn)樾掳娴木€程調(diào)度中有一個(gè)runnext指針指向下次要執(zhí)行的goroutine巷蚪,每次創(chuàng)建goroutine時(shí)會(huì)把當(dāng)前goroutine放進(jìn)去,之前runnext指向的goroutine才會(huì)放入隊(duì)列姻氨。goroutine執(zhí)行的時(shí)候钓辆,會(huì)先取runnext指向的goroutine運(yùn)行,之后才會(huì)從隊(duì)列中順序取出。

如果在每次打印前增加一個(gè)sleep邏輯:

func TestRuntime(t *testing.T) {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(20)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A: ", i)
            wg.Done()
        }()
    }
    for i := 0; i < 10; i++ {
        go func(i int) {
            time.Sleep(time.Second)
            fmt.Println("B: ", i)
            wg.Done()
        }(i)
    }
    
    fmt.Printf("---main end loop---\n")
    wg.Wait()
    fmt.Printf("---  main exit  ---\n")
}

輸出結(jié)果如下:

=== RUN   TestRuntime
---main end loop---
B:  8
B:  9
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
B:  0
B:  1
B:  2
B:  3
B:  4
B:  5
B:  6
B:  7
---  main exit  ---
--- PASS: TestRuntime (0.00s)
PASS

原理是Sleep的時(shí)候會(huì)把所有線程重新放入runnext和Local隊(duì)列前联,sleep結(jié)束之后協(xié)程被依次喚醒并執(zhí)行

2.3 協(xié)程中的異常處理

如果在gouroutine中出現(xiàn)panic:

func main() {
    go func() {
        panic("goroutine panic")
    }()

    log.Println("main done")
}

會(huì)導(dǎo)致main函數(shù)異常終止:

panic: goroutine panic

    goroutine 6 [running]:
    main.main.func1()
            /Users/chenxiangyu/go/src/github.com/go-flowx/awesomeProject/main.go:7 +0x39
    created by main.main
            /Users/chenxiangyu/go/src/github.com/go-flowx/awesomeProject/main.go:6 +0x35

要用defer func保證在當(dāng)前goroutine中recover掉panic

func main() {
    go func() {
        defer func() {
            if e := recover(); e != nil {
                log.Printf("recover: %v", e)
            }
        }()
        panic("goroutine panic")
    }()

    log.Println("main done")
}

3 Sync包

3.1 并發(fā)控制

3.1.1 Mutex

mutex := &sync.Mutex{}

mutex.Lock()
// Update共享變量 (比如切片功戚,結(jié)構(gòu)體指針等)
mutex.Unlock()

3.1.2 RWMutex

mutex := &sync.RWMutex{}

mutex.Lock()
// Update 共享變量
mutex.Unlock()

mutex.RLock()
// Read 共享變量
mutex.RUnlock()

sync.RWMutex是一個(gè)讀寫互斥鎖,允許多個(gè)讀鎖或一個(gè)寫鎖互斥存在似嗤,而sync.Mutex允許一個(gè)讀鎖或一個(gè)寫鎖互斥存在啸臀。

BenchmarkMutexLock-4       83497579         17.7 ns/op
BenchmarkRWMutexLock-4     35286374         44.3 ns/op
BenchmarkRWMutexRLock-4    89403342         15.3 ns/op

可以看到鎖定/解鎖sync.RWMutex讀鎖的速度比鎖定/解鎖sync.Mutex更快,另一方面烁落,在sync.RWMutex上調(diào)用Lock()/ Unlock()是最慢的操作乘粒。
因此,只有在頻繁讀取和不頻繁寫入的場景里伤塌,才應(yīng)該使用sync.RWMutex灯萍。

3.1.3 WaitGroup

sync.WaitGroup也是一個(gè)經(jīng)常會(huì)用到的同步原語,它的使用場景是在一個(gè)goroutine等待一組goroutine執(zhí)行完成每聪。
sync.WaitGroup的數(shù)據(jù)結(jié)構(gòu)非常簡單旦棉,內(nèi)部擁有一個(gè)內(nèi)部計(jì)數(shù)器。當(dāng)計(jì)數(shù)器等于0時(shí)药薯,則Wait()方法會(huì)立即返回绑洛。否則它將阻塞執(zhí)行Wait()方法的goroutine直到計(jì)數(shù)器等于0時(shí)為止。

    // 64 位值: 高 32 位用于計(jì)數(shù)童本,低 32 位用于等待計(jì)數(shù)
    // 64 位的原子操作要求 64 位對(duì)齊真屯,但 32 位編譯器無法保證這個(gè)要求
    // 因此分配 12 字節(jié)然后將他們對(duì)齊,其中 8 字節(jié)作為狀態(tài)穷娱,其他 4 字節(jié)用于存儲(chǔ)原語
    state1 [3]uint32

sync.WaitGroup對(duì)state1變量的操作都是使用atomic包的原子操作實(shí)現(xiàn)的绑蔫,等待時(shí)使用runtime的信號(hào)量實(shí)現(xiàn)阻塞等待:

// Add將增量(可能為負(fù))添加到WaitGroup計(jì)數(shù)器中。
// 如果計(jì)數(shù)器為零鄙煤,則釋放等待時(shí)阻塞的所有g(shù)oroutine晾匠。
// 如果計(jì)數(shù)器變?yōu)樨?fù)數(shù),請(qǐng)?zhí)砑涌只拧?//
// 請(qǐng)注意梯刚,當(dāng)計(jì)數(shù)器為 0 時(shí)發(fā)生的帶有正的 delta 的調(diào)用必須在 Wait 之前凉馆。
// 當(dāng)計(jì)數(shù)器大于 0 時(shí),帶有負(fù) delta 的調(diào)用或帶有正 delta 調(diào)用可能在任何時(shí)候發(fā)生亡资。
// 通常什往,這意味著對(duì)Add的調(diào)用應(yīng)在語句之前執(zhí)行創(chuàng)建要等待的goroutine或其他事件伊诵。
// 如果將WaitGroup重用于等待幾個(gè)獨(dú)立的事件集伪窖,新的Add調(diào)用必須在所有先前的Wait調(diào)用返回之后發(fā)生性芬。
func (wg *WaitGroup) Add(delta int) {
    // 獲取counter,waiter,以及semaphore對(duì)應(yīng)的指針
    statep, semap := wg.state()
    ...
    // 將 delta 加到 statep 的前 32 位上,即加到計(jì)數(shù)器上
    state := atomic.AddUint64(statep, uint64(delta)<<32)
    // 高地址位counter
    v := int32(state >> 32)
    // 低地址為waiter
    w := uint32(state)
    ...
    // 計(jì)數(shù)器不允許為負(fù)數(shù)
    if v < 0 {
        panic("sync: negative WaitGroup counter")
    }
    // wait不等于0說明已經(jīng)執(zhí)行了Wait瘦黑,此時(shí)不容許Add
    if w != 0 && delta > 0 && v == int32(delta) {
        panic("sync: WaitGroup misuse: Add called concurrently with Wait")
    }
    // 計(jì)數(shù)器的值大于或者沒有waiter在等待,直接返回
    if v > 0 || w == 0 {
        return
    }
    // 運(yùn)行到這里只有一種情況 v == 0 && w != 0

    // 這時(shí) Goroutine 已經(jīng)將計(jì)數(shù)器清零京革,且等待器大于零(并發(fā)調(diào)用導(dǎo)致)
    // 這時(shí)不允許出現(xiàn)并發(fā)使用導(dǎo)致的狀態(tài)突變奇唤,否則就應(yīng)該 panic
    // - Add 不能與 Wait 并發(fā)調(diào)用
    // - Wait 在計(jì)數(shù)器已經(jīng)歸零的情況下,不能再繼續(xù)增加等待器了
    // 仍然檢查來保證 WaitGroup 不會(huì)被濫用

    // 這一點(diǎn)很重要匹摇,這段代碼同時(shí)也保證了這是最后的一個(gè)需要等待阻塞的goroutine
    // 然后在下面通過runtime_Semrelease咬扇,喚醒被信號(hào)量semap阻塞的waiter
    if *statep != state {
        panic("sync: WaitGroup misuse: Add called concurrently with Wait")
    }
    // 結(jié)束后將等待器清零
    *statep = 0
    for ; w != 0; w-- {
        // 釋放信號(hào)量,通過runtime_Semacquire喚醒被阻塞的waiter
        runtime_Semrelease(semap, false, 0)
    }
}

// Wait blocks until the WaitGroup counter is zero.
func (wg *WaitGroup) Wait() {
    // 獲取counter,waiter,以及semaphore對(duì)應(yīng)的指針
    statep, semap := wg.state()
    ...
    for {
        // 獲取對(duì)應(yīng)的counter和waiter數(shù)量
        state := atomic.LoadUint64(statep)
        v := int32(state >> 32)
        w := uint32(state)
        // Counter為0廊勃,不需要等待
        if v == 0 {
            if race.Enabled {
                race.Enable()
                race.Acquire(unsafe.Pointer(wg))
            }
            return
        }
        // 原子(cas)增加waiter的數(shù)量(只會(huì)被+1操作一次)
        if atomic.CompareAndSwapUint64(statep, state, state+1) {
            ...
            // 這塊用到了懈贺,我們上文講的那個(gè)信號(hào)量
            // 等待被runtime_Sem release釋放的信號(hào)量喚醒
            // 如果 *semap > 0 則會(huì)減 1,等于0則被阻塞
            runtime_Semacquire(semap)

            // 在這種情況下,如果 *statep 不等于 0 坡垫,則說明使用失誤梭灿,直接 panic
            if *statep != 0 {
                panic("sync: WaitGroup is reused before previous Wait has returned")
            }
            ...
            return
        }
    }
}

3.1.4 Once

sync.Once是一個(gè)簡單而強(qiáng)大的原語,可確保一個(gè)函數(shù)僅執(zhí)行一次冰悠。在下面的示例中堡妒,只有一個(gè)goroutine會(huì)顯示輸出消息:

once := &sync.Once{}
for i := 0; i < 4; i++ {
    i := i
    go func() {
        once.Do(func() {
            fmt.Printf("first %d\n", i)
        })
    }()
}

3.1.5 Cond

sync.Cond可能是sync包提供的同步原語中最不常用的一個(gè),它用于發(fā)出信號(hào)(一對(duì)一)或廣播信號(hào)(一對(duì)多)到goroutine屿脐。讓我們考慮一個(gè)場景涕蚤,我們必須向一個(gè)goroutine指示共享切片的第一個(gè)元素已更新宪卿。創(chuàng)建sync.Cond需要sync.Locker對(duì)象(sync.Mutexsync.RWMutex):

func main() {
    //創(chuàng)建一個(gè)切片
    s := make([]int, 1)
    //創(chuàng)建Cond對(duì)象
    cond := sync.NewCond(&sync.Mutex{})
    for i := 0; i < runtime.NumCPU(); i++ {
        go printFirstElement(s, cond)
    }

    i := get()
    //加鎖的诵,進(jìn)入互斥區(qū)
    cond.L.Lock()
    s[0] = i
    //通知一個(gè)goroutine
    //cond.Signal()
    //通知全部goroutine
    cond.Broadcast()
    //解鎖,退出互斥區(qū)
    cond.L.Unlock() 
}

func printFirstElement(s []int, cond *sync.Cond) {
    cond.L.Lock()
    //等待信號(hào)
    cond.Wait()
    fmt.Printf("%d\n", s[0])
    cond.L.Unlock()
}

3.2 并發(fā)容器

3.2.1 Sync.Map

基本用法和Map一致:

m := &sync.Map{}

// 添加元素
m.Store(1, "one")
m.Store(2, "two")

// 獲取元素1
value, contains := m.Load(1)
if contains {
  fmt.Printf("%s\n", value.(string))
}

// 返回已存value佑钾,否則把指定的鍵值存儲(chǔ)到map中
value, loaded := m.LoadOrStore(3, "three")
if !loaded {
  fmt.Printf("%s\n", value.(string))
}

m.Delete(3)

// 迭代所有元素
m.Range(func(key, value interface{}) bool {
  fmt.Printf("%d: %s\n", key.(int), value.(string))
  return true
})

輸出結(jié)果如下:

one
three
1: one
2: two

數(shù)據(jù)結(jié)構(gòu):



基本原理:

  • 通過read和dirty兩個(gè)字段將讀寫分離西疤,讀的數(shù)據(jù)存在于read字段的,最新寫的數(shù)據(jù)位于dirty字段上休溶。
  • 讀取時(shí)先查詢r(jià)ead代赁,不存在時(shí)查詢dirty,寫入時(shí)只寫入dirty
  • 讀取read不需要加鎖兽掰,而讀或?qū)慸irty需要加鎖
  • 使用misses字段來統(tǒng)計(jì)read被穿透的次數(shù)芭碍,超過一定次數(shù)將數(shù)據(jù)從dirty同步到read上
  • 刪除數(shù)據(jù)通過標(biāo)記來延遲刪除

適用場景:

  • 當(dāng)我們對(duì)map有頻繁的讀取和不頻繁的寫入時(shí)。
  • 當(dāng)多個(gè)goroutine讀取孽尽,寫入和覆蓋不相交的鍵時(shí)窖壕。具體是什么意思呢?例如杉女,如果我們有一個(gè)分片實(shí)現(xiàn)瞻讽,其中包含一組4個(gè)goroutine,每個(gè)goroutine負(fù)責(zé)25%的鍵(每個(gè)負(fù)責(zé)的鍵不沖突)熏挎。在這種情況下速勇,sync.Map是首選。

3.2.2 Sync.Pool

sync.Pool是一個(gè)并發(fā)池坎拐,負(fù)責(zé)安全地保存一組對(duì)象烦磁。
需要注意的是Get()方法會(huì)從并發(fā)池中隨機(jī)取出對(duì)象养匈,無法保證以固定的順序獲取并發(fā)池中存儲(chǔ)的對(duì)象。

pool := &sync.Pool{}

pool.Put(NewConnection(1))
pool.Put(NewConnection(2))
pool.Put(NewConnection(3))

connection := pool.Get().(*Connection)
fmt.Printf("%d\n", connection.id)
connection = pool.Get().(*Connection)
fmt.Printf("%d\n", connection.id)
connection = pool.Get().(*Connection)
fmt.Printf("%d\n", connection.id)

還可以為sync.Pool指定一個(gè)創(chuàng)建者方法:

pool := &sync.Pool{
  New: func() interface{} {
    return NewConnection()
  },
}

connection := pool.Get().(*Connection)

使用場景:

  • 當(dāng)我們必須重用共享的和長期存在的對(duì)象(例如都伪,數(shù)據(jù)庫連接)時(shí)
  • 用于優(yōu)化內(nèi)存分配
func writeFile(pool *sync.Pool, filename string) error {
    buf := pool.Get().(*bytes.Buffer)

  defer pool.Put(buf)

    // Reset 緩存區(qū)乖寒,不然會(huì)連接上次調(diào)用時(shí)保存在緩存區(qū)里的字符串foo
    // 編程foofoo 以此類推
    buf.Reset()

    buf.WriteString("foo")

    return ioutil.WriteFile(filename, buf.Bytes(), 0644)
}

4 額外命令

4.1 compile

go:noinline
表示不做內(nèi)聯(lián)。
好處:

  • 減少函數(shù)調(diào)用的開銷院溺,提高執(zhí)行速度
  • 復(fù)制后的更大函數(shù)體為其他編譯優(yōu)化帶來可能性楣嘁,如過程間優(yōu)化
  • 消除分支,并改善空間局部性和指令順序性珍逸,同樣可以提高性能

壞處:

  • 代碼復(fù)制帶來的空間增長逐虚。
  • 如果有大量重復(fù)代碼,反而會(huì)降低緩存命中率谆膳,尤其對(duì) CPU 緩存是致命的

go:nosplit
跳過棧溢出檢測叭爱。
顯然地,不執(zhí)行棧溢出檢查漱病,可以提高性能买雾,但同時(shí)也有可能發(fā)生 stack overflow 而導(dǎo)致編譯失敗。
go:noescape
不進(jìn)行逃逸分析
最顯而易見的好處是杨帽,GC 壓力變小了漓穿。 因?yàn)樗呀?jīng)告訴編譯器,下面的函數(shù)無論如何都不會(huì)逃逸注盈,那么當(dāng)函數(shù)返回時(shí)晃危,其中的資源也會(huì)一并都被銷毀。 不過老客,這么做代表會(huì)繞過編譯器的逃逸檢查僚饭,一旦進(jìn)入運(yùn)行時(shí),就有可能導(dǎo)致嚴(yán)重的錯(cuò)誤及后果胧砰。
go:norace
跳過競態(tài)檢測鳍鸵,減少編譯時(shí)間。

4.2 runtime

go:systemstack
go:systemstack 表明一個(gè)函數(shù)必須在系統(tǒng)棧上運(yùn)行尉间,這個(gè)會(huì)通過一個(gè)特殊的函數(shù)前引(prologue)動(dòng)態(tài)地驗(yàn)證偿乖。
go:nowritebarrier
go:nowritebarrier 告知編譯器如果以下函數(shù)包含了寫屏障,觸發(fā)一個(gè)錯(cuò)誤(這不會(huì)阻止寫屏障的生成乌妒,只是單純一個(gè)假設(shè))汹想。
一般情況下你應(yīng)該使用 go:nowritebarrierrec。go:nowritebarrier 當(dāng)且僅當(dāng) “最好不要” 寫屏障撤蚊,但是非正確性必須的情況下使用古掏。
go:nowritebarrierrec 與 go:yeswritebarrierrec
go:nowritebarrierrec 告知編譯器如果以下函數(shù)以及它調(diào)用的函數(shù)(遞歸下去),直到一個(gè) go:yeswritebarrierrec 為止侦啸,包含了一個(gè)寫屏障的話槽唾,觸發(fā)一個(gè)錯(cuò)誤丧枪。
邏輯上,編譯器會(huì)在生成的調(diào)用圖上從每個(gè) go:nowritebarrierrec 函數(shù)出發(fā)庞萍,直到遇到了 go:yeswritebarrierrec 的函數(shù)(或者結(jié)束)為止拧烦。如果其中遇到一個(gè)函數(shù)包含寫屏障,那么就會(huì)產(chǎn)生一個(gè)錯(cuò)誤钝计。
go:nowritebarrierrec 主要用來實(shí)現(xiàn)寫屏障自身恋博,用來避免死循環(huán)。
這兩種編譯指令都在調(diào)度器中所使用私恬。寫屏障需要一個(gè)活躍的 P(getg().m.p != nil)债沮,然而調(diào)度器相關(guān)代碼有可能在沒有一個(gè)活躍的 P 的情況下運(yùn)行。在這種情況下本鸣,go:nowritebarrierrec 會(huì)用在一些釋放 P 或者沒有 P 的函數(shù)上運(yùn)行疫衩,go:yeswritebarrierrec 會(huì)用在重新獲取到了 P 的代碼上。因?yàn)檫@些都是函數(shù)級(jí)別的注釋荣德,所以釋放 P 和獲取 P 的代碼必須被拆分成兩個(gè)函數(shù)闷煤。
go:notinheap
go:notinheap 適用于類型聲明,表明了一個(gè)類型必須不被分配在 GC 堆上涮瞻。特別的鲤拿,指向該類型的指針總是應(yīng)當(dāng)在 runtime.inheap 判斷中失敗。這個(gè)類型可能被用于全局變量饲宛、棧上變量皆愉,或者堆外內(nèi)存上的對(duì)象(比如通過 sysAlloc、persistentalloc艇抠、fixalloc 或者其它手動(dòng)管理的 span 進(jìn)行分配)。特別的:
● new(T)久锥、make([]T)家淤、append([]T, ...) 和隱式的對(duì)于 T 的堆上分配是不允許的(盡管隱式的分配在 runtime 中是從來不被允許的)。
● 一個(gè)指向普通類型的指針(除了 unsafe.Pointer)不能被轉(zhuǎn)換成一個(gè)指向 go:notinheap 類型的指針瑟由,就算它們有相同的底層類型(underlying type)絮重。
● 任何一個(gè)包含了 go:notinheap 類型的類型自身也是 go:notinheap 的。如果結(jié)構(gòu)體和數(shù)組包含 go:notinheap 的元素歹苦,那么它們自身也是 go:notinheap 類型青伤。map 和 channel 不允許有 go:notinheap 類型。為了使得事情更加清晰殴瘦,任何隱式的 go:notinheap 類型都應(yīng)該顯式地標(biāo)明 go:notinheap狠角。
● 指向 go:notinheap 類型的指針的寫屏障可以被忽略。
最后一點(diǎn)是 go:notinheap 類型真正的好處蚪腋。runtime 在底層結(jié)構(gòu)中使用這個(gè)來避免調(diào)度器和內(nèi)存分配器的內(nèi)存屏障以避免非法檢查或者單純提高性能丰歌。這種方法是適度的安全(reasonably safe)的并且不會(huì)使得 runtime 的可讀性降低姨蟋。
go:linkname localname linkname
編譯時(shí)將外部包私有成員函數(shù)或變量連接到當(dāng)前類,常量不可用立帖。

//go:linkname sayTest a.say 
func sayTest(name string) string
func Greet(name string) string {
    return sayTest(name)
}

noCopy
noCopy是一個(gè)golang基礎(chǔ)包內(nèi)部的結(jié)構(gòu)體眼溶,用于禁止sync結(jié)構(gòu)體被復(fù)制

type noCopy struct{}

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

type DoNotCopyMe struct {
  noCopy
}

var d DoNotCopyMe
d2 := d // 這里發(fā)生了拷貝

// 假設(shè)我們上面的代碼寫在main.go文件中
go vet main.go
// 會(huì)看到如下輸出
./main.go:616:11: assignment copies lock value to d2: command-line-arguments.DoNotCopyMe contains command-line-arguments.noCopy
./main.go:618:23: call of fmt.Printf copies lock value: command-line-arguments.DoNotCopyMe contains command-line-arguments.noCopy
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市晓勇,隨后出現(xiàn)的幾起案子堂飞,更是在濱河造成了極大的恐慌,老刑警劉巖绑咱,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酝静,死亡現(xiàn)場離奇詭異,居然都是意外死亡羡玛,警方通過查閱死者的電腦和手機(jī)别智,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來稼稿,“玉大人薄榛,你說我怎么就攤上這事∪眉撸” “怎么了敞恋?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長谋右。 經(jīng)常有香客問我硬猫,道長,這世上最難降的妖魔是什么改执? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任啸蜜,我火速辦了婚禮,結(jié)果婚禮上辈挂,老公的妹妹穿的比我還像新娘衬横。我一直安慰自己,他們只是感情好终蒂,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布蜂林。 她就那樣靜靜地躺著,像睡著了一般拇泣。 火紅的嫁衣襯著肌膚如雪噪叙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天霉翔,我揣著相機(jī)與錄音睁蕾,去河邊找鬼。 笑死早龟,一個(gè)胖子當(dāng)著我的面吹牛惫霸,可吹牛的內(nèi)容都是我干的猫缭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼壹店,長吁一口氣:“原來是場噩夢啊……” “哼猜丹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起硅卢,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤射窒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后将塑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脉顿,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年点寥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了艾疟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡敢辩,死狀恐怖蔽莱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情戚长,我是刑警寧澤盗冷,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站同廉,受9級(jí)特大地震影響仪糖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜迫肖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一锅劝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧咒程,春花似錦鸠天、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奶段。三九已至饥瓷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間痹籍,已是汗流浹背呢铆。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蹲缠,地道東北人棺克。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓悠垛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親娜谊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子确买,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容