使用 go 理解 Lock-Free
Lock-Free(無鎖程式設計)
鎖是程式設計中常用的技術, 通常應用於共享記憶體, 多個執行緒向同一資源操作往往會發生很多問題, 為了防止這些問題只能用到鎖解決. 雖然鎖可以解決, 但是在高併發的場景下, 可能會造成效能瓶頸.
無鎖程式設計目前大多數都是基於atomic
實現, atomic
能夠保證資料的正確性, sync.Mutex
也有 Lock-Free 的影子.
無鎖程式設計是什麼?
<<The Art of Multiprocessor Programming>>書中的定義:
"如果一個方法是無鎖的,它保證執行緒無限次呼叫這個方法都能夠在有限步內完成。"
成為無鎖的條件:
- 是多執行緒.
- 多個執行緒訪問共享記憶體.
- 不會令其它執行緒造成阻塞.
go 中如果有一個方法裡操作棧資料, 如果沒有鎖肯定會導致競爭發生, 加上鎖又不會是無鎖. 無鎖程式設計是一個既複雜又具有挑戰性的活, 究竟如何寫一個無鎖程式碼?
實現 Lock-Free
type Config struct {
sync.RWMutex
endpoint string
}
func BenchmarkPMutexSet(b *testing.B) {
config := Config{}
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
config.Lock()
config.endpoint = "api.example.com"
config.Unlock()
}
})
}
func BenchmarkPMutexGet(b *testing.B) {
config := Config{endpoint: "api.example.com"}
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
config.RLock()
_ = config.endpoint
config.RUnlock()
}
})
}
func BenchmarkPAtomicSet(b *testing.B) {
var config atomic.Value
c := Config{endpoint: "api.example.com"}
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
config.Store(c)
}
})
}
func BenchmarkPAtomicGet(b *testing.B) {
var config atomic.Value
config.Store(Config{endpoint: "api.example.com"})
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = config.Load().(Config)
}
})
}
看看結果
BenchmarkPMutexSet-8 19403011 61.6 ns/op 0 B/op 0 allocs/op
BenchmarkPMutexGet-8 35671380 32.7 ns/op 0 B/op 0 allocs/op
BenchmarkPAtomicSet-8 32477751 37.0 ns/op 48 B/op 1 allocs/op
BenchmarkPAtomicGet-8 1000000000 0.247 ns/op 0 B/op 0 allocs/op
比較結果相當明確, 確實是快. 上面只是一個最簡單的實現, 看看 Lock-Free Stack.
實現 Lock-Free Stack
先看一下鎖實現的棧
var mu sync.Mutex
type LStack struct {
Next *LStack
Item int
}
func (head *LStack) Push(i int) {
mu.Lock()
defer mu.Unlock()
new := &LStack{Item: i}
new.Next = head.Next
head.Next = new
}
func (head *LStack) Pop() int {
mu.Lock()
defer mu.Unlock()
old := head.Next
if old == nil {
return 0
}
new := head.Next
head.Next = new
return old.Item
}
LStack
實現Push
和Pop
方法, 兩個方法都加上鎖, 防止競爭.
下面是 Lock-Free Stack
type LFStack struct {
Next unsafe.Pointer
Item int
}
var lfhead unsafe.Pointer // 記錄棧頭資訊
func (head *LFStack) Push(i int) *LFStack { // 強制逃逸
new := &LFStack{Item: i}
newptr := unsafe.Pointer(new)
for {
old := atomic.LoadPointer(&lfhead)
new.Next = old
if atomic.CompareAndSwapPointer(&lfhead, old, newptr) {
break
}
}
return new
}
func (head *LFStack) Pop() int {
for {
time.Sleep(time.Nanosecond) // 可以讓CPU緩一緩
old := atomic.LoadPointer(&lfhead)
if old == nil {
return 0
}
if lfhead == old {
new := (*LFStack)(old).Next
if atomic.CompareAndSwapPointer(&lfhead, old, new) {
return 1
}
}
}
}
LFStack
也實現了Push
和Pop
方法, 雖然沒有加鎖, 也可以保證返回資料的正確性. 對比鎖實現的方法來看, 是邏輯要複雜得多. 由於迴圈使 CPU 壓力增大, 可以用time.Sleep
暫停一下.
runtime/lfstack.go
最近在研究 gc 時發現 go 原始碼有用到 Lock-Free Stack, 在runtime/lfstack.go
type lfstack uint64
func (head *lfstack) push(node *lfnode) {
node.pushcnt++
new := lfstackPack(node, node.pushcnt)
if node1 := lfstackUnpack(new); node1 != node {
print("runtime: lfstack.push invalid packing: node=", node, " cnt=", hex(node.pushcnt), " packed=", hex(new), " -> node=", node1, "\n")
throw("lfstack.push")
}
for {
old := atomic.Load64((*uint64)(head))
node.next = old
if atomic.Cas64((*uint64)(head), old, new) {
break
}
}
}
func (head *lfstack) pop() unsafe.Pointer {
for {
old := atomic.Load64((*uint64)(head))
if old == 0 {
return nil
}
node := lfstackUnpack(old)
next := atomic.Load64(&node.next)
if atomic.Cas64((*uint64)(head), old, next) {
return unsafe.Pointer(node)
}
}
}
func (head *lfstack) empty() bool {
return atomic.Load64((*uint64)(head)) == 0
}
func lfnodeValidate(node *lfnode) {
if lfstackUnpack(lfstackPack(node, ^uintptr(0))) != node {
printlock()
println("runtime: bad lfnode address", hex(uintptr(unsafe.Pointer(node))))
throw("bad lfnode address")
}
}
lfstack
主要是用於對 gc 時儲存灰色物件, 有興趣的可以看看.
小結
Lock-Free 的實現還有很多種, Lock-Free Stack 只是其中之一. 在日常的程式設計中, 基本上用sync.Mutex
可以滿足需求, 不要強制專案使用 Lock-Free, 可以選擇在負載高的方法考慮使用, 由於實現複雜有可能效能也不及鎖.
在 benchmark 測試LFStack
和LStack
發現, 前者的效能不及後者, 所以不是無鎖都好用. 如果大家有興趣可以研究一下無鎖佇列.
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- go中Tag的理解與使用Go
- 使用 Go 語言來理解 TensorflowGo
- 重新理解 Go 培訓和 Go 人才Go
- 理解 Go Channels[精品長文]Go
- 深入理解 Go MapGo
- 理解 go mod init 命令Go
- 深入理解Go ContextGoContext
- 理解 Go 中的協程(Goroutine)Go
- 用 Go 語言理解 TensorflowGo
- 上篇 | 說說無鎖(Lock-Free)程式設計那些事程式設計
- 下篇 | 說說無鎖(Lock-Free)程式設計那些事(下)程式設計
- Go 切片詳解(理解是關鍵)Go
- Go GPM的理解 與 runtime包Go
- 深入理解Go語言的sliceGo
- go template使用Go
- Go Rabbitmq 使用GoMQ
- go mod 使用Go
- go slice使用Go
- Go 模組--開始使用 Go ModulesGo
- 深入理解Go-垃圾回收機制Go
- [go語言]-深入理解singleflightGo
- Go記憶體分配和GC的理解Go記憶體GC
- go中map的資料結構理解Go資料結構
- 基於Go語言來理解TensorflowGo
- 基於 Go 語言來理解 TensorflowGo
- vuex使用理解Vue
- go使用grpcGoRPC
- 如何使用go文件Go
- go語言使用Go
- 深入理解GO語言之併發機制Go
- Actor model 的理解與 protoactor-go 的分析Go
- go開源庫之jwt-go使用GoJWT
- Go 包管理歷史以及 Go mod 使用Go
- 用“揹包”去理解Go語言中的閉包Go
- Go學習【02】:理解Gin,搭一個web demoGoWeb
- 深入理解Go系列一之指標變數Go指標變數
- 深入理解 Go 中的 new() 和 make() 函式Go函式
- 深入理解GO語言之記憶體詳解Go記憶體