Goroutine
go語言的魅力所在,高并發。
線程是操作系統調度的一種執行路徑,用于在處理器執行我們在函數中編寫的代碼。一個進程從一個線程開始,即主線程,當該線程終止時,進程終止。這是因為主線程是應用程序的原點。然后,主線程可以依次啟動更多的線程,而這些線程可以啟動更多的線程。
并發不是并行。并行是指兩個或多個線程同時在不同的處理器執行代碼。如果將運行時配置為使用多個邏輯處理器,則調度程序將在這些邏輯處理器之間分配 goroutine,這將導致 goroutine 在不同的操作系統線程上運行。但是,要獲得真正的并行性,您需要在具有多個物理處理器的計算機上運行程序。否則,goroutine 將針對單個物理處理器并發運行,即使 Go 運行時使用多個邏輯處理器。
注意:① 空的 select{} 會永遠進行 阻塞
② 如果當前的goroutine要執行但是需要等待另一個goroutine執行完畢才行,這時候就要把另一個goroutine的事情拿來自己做。
③ 使用goroutine考慮兩個問題,一是它什么時候結束,二是你有沒有一個方法讓它結束
這里展示一個較好例子:
Memory model
go 的內存模型,個人總結:只要是多線程就不要覺得自己太聰明,使用”鎖",確保操作事物的原子性,因為多線程之間的調度過程中,cpu,線程為了最大限度的性能,有可能改變不同線程之間關于整體事物的邏輯執行順序,導致結果出現不可測性。指針copy的方法,指針是一個原子性,但是并不滿足 single machine. 當然如果你只是單線程,那么這些憂慮不用考慮
為了說明讀和寫的必要條件,我們定義了先行發生(Happens Before)。如果事件 e1 發生在 e2 前,我們可以說 e2 發生在 e1 后。如果 e1不發生在 e2 前也不發生在 e2 后,我們就說 e1 和 e2 是并發的。
在單一的獨立的 goroutine 中先行發生的順序即是程序中表達的順序。
- 當下面條件滿足時,對變量 v 的讀操作 r 是被允許看到對 v 的寫操作 w 的:
- r 不先行發生于 w
- 在 w 后 r 前沒有對 v 的其他寫操作
- 為了保證對變量 v 的讀操作 r 看到對 v 的寫操作 w,要確保 w 是 r 允許看到的唯一寫操作。即當下面條件滿足時,r 被保證看到 w:
- w 先行發生于 r
- 其他對共享變量 v 的寫操作要么在 w 前,要么在 r 后。
這一對條件比前面的條件更嚴格,需要沒有其他寫操作與 w 或 r 并發發生。
Package sync
data race : 線程競爭,在線程模型中Go 沒有顯式地使用鎖來協調對共享數據的訪問,而是鼓勵使用 chan 在 goroutine 之間傳遞對數據的引用。這種方法確保在給定的時間只有一個 goroutine 可以訪問數據。
go buid -race go test -race
i++是原子性操作嗎? 不是,i++的底層是三行代碼,并不是原子性。
Copy-On-Write 思路在微服務降級或者 local cache 場景中經常使用。寫時復制指的是,寫操作時候復制全量老數據到一個新的對象中,攜帶上本次新寫的數據,之后利用原子替換(atomic.Value),更新調用者的變量。來完成無鎖訪問共享數據。這也是 Redis 進行寫操作來更改數據的一種方法。
Mutex
- go的幾種 Mutex 鎖的實現:
- Barging. 這種模式是為了提高吞吐量,當鎖被釋放時,它會喚醒第一個等待者,然后把鎖給第一個等待者或者給第一個請求鎖的人。
- Handsoff. 當鎖釋放時候,鎖會一直持有直到第一個等待者準備好獲取鎖。它降低了吞吐量,因為鎖被持有,即使另一個 goroutine 準備獲取它。(一個互斥鎖的 handsoff 會完美地平衡兩個goroutine 之間的鎖分配,但是會降低性能,因為它會迫使第一個 goroutine 等待鎖。)
- Spinning. 自旋在等待隊列為空或者應用程序重度使用鎖時效果不錯。parking 和 unparking goroutines 有不低的性能成本開銷,相比自旋來說要慢得多。
errgroup
實際生活中我們的errgroup使用的更多一些,用幾個gooroutine 去進行分布處理業務,然后通過sync進行控制管理各個分支,把最后的數據再集合起來。 sync.waitGroup()
Chan
各個goroutine通過chan管道來進行實時通信。
channels 是一種類型安全的消息隊列,充當兩個 goroutine 之間的管道,將通過它同步的進行任意資源的交換。chan 控制 goroutines 交互的能力從而創建了 Go 同步機制。當創建的 chan 沒有容量時,稱為無緩沖通道。反過來,使用容量創建的 chan 稱為緩沖通道。
無緩沖chan
ch := make(chan struct{})
無緩沖 chan 沒有容量,因此進行任何交換前需要兩個 goroutine 同時準備好。當 goroutine 試圖將一個資源發送到一個無緩沖的通道并且沒有goroutine 等待接收該資源時,該通道將鎖住發送 goroutine 并使其等待。當 goroutine 嘗試從無緩沖通道接收,并且沒有 goroutine 等待發送資源時,該通道將鎖住接收 goroutine 并使其等待。
意思就是雙方都得準備好才能進行下去 , 無緩沖信道的本質是保證同步。
- Receive 先于 Send 發生。
- 好處: 100% 保證能收到。
- 代價: 延遲時間未知。
有緩沖的chan
buffered channel 具有容量,因此其行為可能有點不同。當 goroutine 試圖將資源發送到緩沖通道,而該通道已滿時,該通道將鎖住 goroutine并使其等待緩沖區可用。如果通道中有空間,發送可以立即進行,goroutine 可以繼續。當goroutine 試圖從緩沖通道接收數據,而緩沖通道為空時,該通道將鎖住 goroutine 并使其等待資源被發送。
- Send 先于 Receive 發生。
- 好處: 延遲更小。
- 代價: 不保證數據到達,越大的 buffer,越小的保障到達。buffer = 1 時,給你延遲一個消息的保障。
Context
context 的使用因該是貫穿全過程,而不是被把它放到一個對象結構體中去使用
context.WithValue() : 用于創建一個context對象
context.WithValue 方法允許上下文攜帶請求范圍的數據。這些數據必須是安全的,以便多個 goroutine 同時使用。這里的數據,更多是面向請求的元數據,不應該作為函數的可選參數來使用(比如 context 里面掛了一個sql.Tx 對象,傳遞到 data 層使用),因為元數據相對函數參數更加是隱含的,面向請求的。而參數是更加顯示的。
同一個 context 對象可以傳遞給在不同 goroutine 中運行的函數;上下文對于多個 goroutine 同時使用是安全的。對于值類型最容易犯錯的地方,在于 context value 應該是 immutable 的,每次重新賦值應該是新的 context,即: context.WithValue(ctx, oldvalue)
比如 染色,API 重要性,Trace
注意選擇使用 copy-on-write 的思路,解決跨多個 goroutine 使用數據、修改數據的場景。
COW: 從 ctx1 中獲取 map1(可以理解為 v1 版本的 map 數據)。構建一個新的 map 對象 map2,復制所有 map1 數據,同時追加新的數據 “k2”: “v2” 鍵值對,使用 context.WithValue 創建新的 ctx2,ctx2 會傳遞到其他的 goroutine 中。這樣各自讀取的副本都是自己的數據,寫行為追加的數據,在 ctx2 中也能完整讀取到,同時也不會污染 ctx1 中的數據。
COW就是 copy-on-write
當一個 context 被取消時,從它派生的所有 context 也將被取消。WithCancel(ctx) 參數 ctx 認為是 parent ctx,在內部會進行一個傳播關系鏈的關聯。Done() 返回 一個 chan,當我們取消某個parent context, 實際上上會遞歸層層 cancel 掉自己的 child context 的 done chan 從而讓整個調用鏈中所有監聽 cancel 的 goroutine 退出。
如果要實現一個超時控制,通過上面的 context 的 parent/child 機制,其實我們只需要啟動一個定時器,然后在超時的時候,直接將當前的 context 給 cancel 掉,就可以實現監聽在當前和下層的額 context.Done() 的 goroutine 的退出。