This page looks best with JavaScript enabled

Concurrency in Go - I 前三章

 ·  ☕ 3 min read

這三章節內容其實滿少的,讀過之後隨便記一記

ch1

Why Is Concurrency Hard?
Race condition
應該要依序執行的不同指定們,沒有依照正確的順序造成結果錯誤
例如下面這段程式就有可能出現很多結果,因為使用者可能以為go裡面的程式碼寫在前面,就會先跑

1
2
3
4
5
6
7
var a int
go func(){
    a++
}()
if a==0{
    fmt.Println("zero")
}

Atomicity
不可切割,安全,一次要做完

Memory Access Synchronization
不同地方可能 concurrently 存取同一個記憶體,需要變成 critical section 用鎖鎖住之類的,但會變慢

Deadlocks, Livelocks, and Starvation

dead lock -> 4個條件

Determining Concurrency Safety
• Who is responsible for the concurrency?
• How is the problem space mapped onto concurrency primitives?
• Who is responsible for the synchronization?

ch2

Go CSP
下圖是 Concurrency in Go 33 頁的圖
primitives 指的像是 sync package 裡面的 mutex

四個條件的解釋

  • Are you trying to transfer ownership of data?
    盡量維持一次只有一個人有資料的擁有權利,做完一件事後要把output丟給別人這種動作,適合用 channel

  • Are you trying to guard internal state of a struct?
    把實作藏起來不讓外面看到 -> 用primitives

  • Are you trying to coordinate multiple pieces of logic?
    到處都有 lock 很容易造成問題,而 channel 的組合性很好,出錯也好抓,複雜的邏輯最好用 channel 來做 而不是自己到處鎖來鎖去

  • Is it a performance-critical section?
    真的發現使用 channel 造成巨大效能問題才改用 primitives,而不是為了效能一開始就用 primitives

ch3

goroutine 不能被 interrupt?
以前好像是,後來改成可以,不然垃圾回收之類的功能有可能被卡住
但大多還是在固定時間才會被暫停,例如 sleep, 等 io 之類的

goroutine closure 在同一個 address space 執行

	var wg sync.WaitGroup
	salutation := "hello"
	wg.Add(1)
	go func() {
		defer wg.Done()
		salutation = "welcome"
	}()
	wg.Wait()
	fmt.Println(salutation)

The sync package

WaitGroup

    var wg sync.WaitGroup // 傳進 func 時要傳 pointer
    wg.Add(2) // +2
    defer wg.Done() // -1
    wg.Wait() // == 0 時完成

mutex

    var lock sync.Mutex
    lock.Lock()
    defer lock.Unlock()
    
    var m sync.RWMutex
    m.Lock() // rw lock
    m.RLock() // r lock
    m.RLocker() // r locker 

Cond

有效率的等待,不用浪費CPU跑迴圈

c := sync.NewCond(&sync.Mutex{}) 
c.L.Lock()
for conditionTrue() == false {
    c.Wait()  // suspend, 其他 goroutine 可以用 thread
}
c.L.Unlock()


c.Singal() 去戳正在 Wait 的一個 goroutine (最早開始等的)
c.Broadcast() 去戳所有在 Wait 的 goroutines

signal 可以用 channel 來做出一樣的功能
但 broadcast 會比較麻煩,也可能需要很多操作

once

var once sync.Once
once.Do(xxx)

即使一堆 goroutine,也保證 once 只會做一件事情

下面這種也是只會執行aaa

once.Do(aaa)
once.Do(bbb)

計數是記once 做過的事情而不是 unique function 數

Pool

創造及持有東西很貴 不希望有太多 所以放在一個 pool 重複運用並限制數量

提前把一些東西塞進 pool 避免 create,像是 db 連線等等

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
myPool := &sync.Pool{
		New: func() interface{} {
			fmt.Println("Creating new instance.")
			return struct{}{}
		},
	}
	myPool.Get()
	instance := myPool.Get()
	myPool.Put(instance)
	myPool.Get()

使用方法:

  1. 加入 New func
  2. Get 的東西有可能是任何狀態
  3. 記得 Put 回去,通常用 defer
  4. 裡面的東西基本上要長得一樣

Channels

    var dataStream chan int
    var dataStream <-chan int
    var dataStream chan<- int
    dataStream = make(<-chan int, 1) 
    
    很少會 make 單向的 chan,通常是傳參數時會用
    dataStream := make(chan int)
    var d chan<- int
    d = dataStream

關閉 channel
之後還是可以無限讀取,因為可能很多下游的 goroutine 都需要讀

	dataStream := make(chan int)
	go func() {
		close(dataStream)

	}()
	d, ok := <-dataStream
	fmt.Println("I GET! ", d, ok) // 0, false
收到 close 時會直接 break
for integer := range intStream { 
    fmt.Printf("%v ", integer)
}

p.75 列出了不同狀態的 chan 上操作會出現什麼情況,包括錯誤們

p. 91 有範例
要用好 channel 有幾點

  1. 確認 channel 的 owner 是誰(盡量少)
  2. owner 負責 channel 的創造、寫、關、轉移 ownership
  3. 非 owner 負責讀,確認 chan 何時關、處理 blocking

例如第 2 點,把 owner 限制在很小的範圍,就可以避免寫入 nil chan、close closed chan 之類的 p.75 所提的程式邏輯 bug

Select

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    v1 := make(chan int)
    go func() {
        time.Sleep(3 * time.Second)
        close(v1)
    }()


    select {
    case x := <-v1: // v1 有東西拿時
        // xxx
    case v1 <- 1:   // v1 有空間時
       // ooo
    }

當多個case都可以時會隨機選一個

timeout
case <-time.After(1 * time.Second):

default (通常和for一起用) 其他 case 都沒中的時候
default:

Share on

Marko Peng
WRITTEN BY
Marko Peng
Good man