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
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):
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()
}
整體來看舅踪,兩者的速度相差不大
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ì)引起抽碌。所以應(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ì)解除這樣的鎖定漩蟆。
注意:
- 多次調(diào)用前者不會(huì)出現(xiàn)任何問題,但最后一次調(diào)用的記錄會(huì)被保留妓蛮,
- 即時(shí)之前沒有調(diào)用前者怠李,對(duì)后者的調(diào)用也不會(huì)產(chǎn)生任何副作用
- 綁定后G創(chuàng)建的子G不會(huì)被綁定
- time.Sleep等操作建議改為等效的syscall操作,否則這些操作效果可能達(dá)不到預(yù)期
runtime.SetFinalizer函數(shù)
類似于Java中的finalize函數(shù)蛤克,會(huì)在對(duì)象沒有引用且GC準(zhǔn)備回收該對(duì)象時(shí)被調(diào)用捺癞。
- 即使程序正常結(jié)束或者發(fā)生錯(cuò)誤, 但是在對(duì)象被 gc 選中并被回收之前构挤,SetFinalizer 都不會(huì)執(zhí)行髓介, 所以不要在SetFinalizer中執(zhí)行將內(nèi)存中的內(nèi)容flush到磁盤這種操作
- SetFinalizer 最大的問題是延長了對(duì)象生命周期。在第一次回收時(shí)執(zhí)行 Finalizer 函數(shù)筋现,且目標(biāo)對(duì)象重新變成可達(dá)狀態(tài)唐础,直到第二次才真正 “銷毀”。這對(duì)于有大量對(duì)象分配的高并發(fā)算法矾飞,可能會(huì)造成很大麻煩
- 指針構(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.Mutex
或sync.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