自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何使用 atomic 包減少鎖沖突

開發(fā) 前端
Go 提供了 channel 或 mutex 等內(nèi)存同步機(jī)制,有助于解決不同的問題。在共享內(nèi)存的情況下,mutex 可以保護(hù)內(nèi)存不發(fā)生數(shù)據(jù)競(jìng)爭(zhēng)(data race)。不過,雖然存在兩個(gè) mutex,但 Go 也通過 atomic 包提供了原子內(nèi)存基元來提高性能。在深入研究解決方案之前,我們先回過頭來看看數(shù)據(jù)競(jìng)爭(zhēng)。

寫在前面

本文基于 Golang 1.14

Go 提供了 channel 或 mutex 等內(nèi)存同步機(jī)制,有助于解決不同的問題。在共享內(nèi)存的情況下,mutex 可以保護(hù)內(nèi)存不發(fā)生數(shù)據(jù)競(jìng)爭(zhēng)(data race)。不過,雖然存在兩個(gè) mutex,但 Go 也通過 atomic 包提供了原子內(nèi)存基元來提高性能。在深入研究解決方案之前,我們先回過頭來看看數(shù)據(jù)競(jìng)爭(zhēng)。

數(shù)據(jù)競(jìng)爭(zhēng)

當(dāng)兩個(gè)或兩個(gè)以上的 goroutine 同時(shí)訪問同一塊內(nèi)存區(qū)域,并且其中至少有一個(gè)在寫時(shí),就會(huì)發(fā)生數(shù)據(jù)競(jìng)爭(zhēng)。雖然 map 內(nèi)部有一定的機(jī)制來防止數(shù)據(jù)競(jìng)爭(zhēng),但一個(gè)簡(jiǎn)單的結(jié)構(gòu)體并沒有任何的機(jī)制,因此容易發(fā)生數(shù)據(jù)競(jìng)爭(zhēng)。

為了說明數(shù)據(jù)競(jìng)爭(zhēng),我以一個(gè)goroutine 持續(xù)更新的配置為例向大家展示一下。 

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "sync" 
  6.  
  7. type Config struct { 
  8.     a []int 
  9.  
  10. func main() { 
  11.     cfg := &Config{} 
  12.  
  13.     // 啟動(dòng)一個(gè) writer goroutine,不斷寫入數(shù)據(jù) 
  14.     go func() { 
  15.         i := 0 
  16.  
  17.         for { 
  18.             i++ 
  19.             cfg.a = []int{i, i + 1, i + 2, i + 3, i + 4, i + 5} 
  20.         } 
  21.     }() 
  22.  
  23.     // 啟動(dòng)多個(gè) reader goroutine,不斷獲取數(shù)據(jù) 
  24.     var wg sync.WaitGroup 
  25.     for n := 0; n < 4; n++ { 
  26.         wg.Add(1) 
  27.         go func() { 
  28.             for n := 0; n < 100; n++ { 
  29.                 fmt.Printf("%#v\n", cfg) 
  30.             } 
  31.             wg.Done() 
  32.         }() 
  33.     } 
  34.  
  35.     wg.Wait() 

運(yùn)行這段代碼可以清楚地看到,原本期望是運(yùn)行上述代碼后,每一行的數(shù)字應(yīng)該是連續(xù)的,但是由于數(shù)據(jù)競(jìng)爭(zhēng)的存在,導(dǎo)致結(jié)果是非確定性的。 

  1. F:\hello>go run main.go 
  2. [...] 
  3. &main.Config{a:[]int{180954, 180962, 180967, 180972, 180977, 180983}} 
  4. &main.Config{a:[]int{181296, 181304, 181311, 181318, 181322, 181323}} 
  5. &main.Config{a:[]int{181607, 181617, 181624, 181631, 181636, 181643}} 

我們可以在運(yùn)行時(shí)加入?yún)?shù) --race 看一下結(jié)果: 

  1. F:\hello>go run --race main.go 
  2. [...] 
  3. &main.Config{a:[]int(nil)} 
  4. ================== 
  5. &main.Config{a:[]int(nil)} 
  6. WARNING: DATA RACE&main.Config{a:[]int(nil)} 
  7.  
  8. Read at 0x00c00000c210 by Goroutine 9: 
  9.   reflect.Value.Int() 
  10.       D:/Go/src/reflect/value.go:988 +0x3584 
  11.   fmt.(*pp).printValue() 
  12.       D:/Go/src/fmt/print.go:749 +0x3590 
  13.   fmt.(*pp).printValue() 
  14.       D:/Go/src/fmt/print.go:860 +0x8f2 
  15.   fmt.(*pp).printValue() 
  16.       D:/Go/src/fmt/print.go:810 +0x289a 
  17.   fmt.(*pp).printValue() 
  18.       D:/Go/src/fmt/print.go:880 +0x261c 
  19.   fmt.(*pp).printArg() 
  20.       D:/Go/src/fmt/print.go:716 +0x26b 
  21.   fmt.(*pp).doPrintf() 
  22.       D:/Go/src/fmt/print.go:1030 +0x326 
  23.   fmt.Fprintf() 
  24.       D:/Go/src/fmt/print.go:204 +0x86 
  25.   fmt.Printf() 
  26.       D:/Go/src/fmt/print.go:213 +0xbc 
  27.   main.main.func2() 
  28.       F:/hello/main.go:31 +0x42 
  29.  
  30. Previous write at 0x00c00000c210 by goroutine 7: 
  31.   main.main.func1() 
  32.       F:/hello/main.go:21 +0x66 
  33.  
  34. goroutine 9 (running) created at
  35.   main.main() 
  36.       F:/hello/main.go:29 +0x124 
  37.  
  38. goroutine 7 (running) created at
  39.   main.main() 
  40.       F:/hello/main.go:16 +0x95 
  41. ================== 

為了避免同時(shí)讀寫過程中產(chǎn)生的數(shù)據(jù)競(jìng)爭(zhēng)最常采用的方法可能是使用 mutex 或 atomic 包。

Mutex?還是 Atomic?

標(biāo)準(zhǔn)庫在 sync 包提供了兩種互斥鎖 :sync.Mutex 和 sync.RWMutex。后者在你的程序需要處理多個(gè)讀操作和極少的寫操作時(shí)進(jìn)行了優(yōu)化。

針對(duì)上面代碼中產(chǎn)生的數(shù)據(jù)競(jìng)爭(zhēng)問題,我們看一下,如何解決呢?

使用 sync.Mutex 解決數(shù)據(jù)競(jìng)爭(zhēng) 

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "sync" 
  6.  
  7. // Config 定義一個(gè)結(jié)構(gòu)體用于模擬存放配置數(shù)據(jù) 
  8. type Config struct { 
  9.     a []int 
  10.  
  11. func main() { 
  12.     cfg := &Config{} 
  13.     var mux sync.RWMutex 
  14.  
  15.     // 啟動(dòng)一個(gè) writer goroutine,不斷寫入數(shù)據(jù) 
  16.     go func() { 
  17.         i := 0 
  18.  
  19.         for { 
  20.             i++ 
  21.             // 進(jìn)行數(shù)據(jù)寫入時(shí),先通過鎖進(jìn)行鎖定 
  22.             mux.Lock() 
  23.             cfg.a = []int{i, i + 1, i + 2, i + 3, i + 4, i + 5} 
  24.             mux.Unlock() 
  25.         } 
  26.     }() 
  27.  
  28.     // 啟動(dòng)多個(gè) reader goroutine,不斷獲取數(shù)據(jù) 
  29.     var wg sync.WaitGroup 
  30.     for n := 0; n < 4; n++ { 
  31.         wg.Add(1) 
  32.         go func() { 
  33.             for n := 0; n < 100; n++ { 
  34.                 // 因?yàn)檫@里只是需要讀取數(shù)據(jù),所以只需要加一個(gè)讀鎖即可 
  35.                 mux.RLock() 
  36.                 fmt.Printf("%#v\n", cfg) 
  37.                 mux.RUnlock() 
  38.             } 
  39.             wg.Done() 
  40.         }() 
  41.     } 
  42.  
  43.     wg.Wait() 

通過上面的代碼,我們做了兩處改動(dòng)。第一處改動(dòng)在寫數(shù)據(jù)前通過 mux.Lock() 加了一把鎖;第二處改動(dòng)在讀數(shù)據(jù)前通過 mux.RLock() 加了一把讀鎖。

運(yùn)行上述代碼看一下結(jié)果: 

  1. F:\hello>go run --race main.go 
  2. &main.Config{a:[]int{512, 513, 514, 515, 516, 517}} 
  3. &main.Config{a:[]int{512, 513, 514, 515, 516, 517}} 
  4. &main.Config{a:[]int{513, 514, 515, 516, 517, 518}} 
  5. &main.Config{a:[]int{513, 514, 515, 516, 517, 518}} 
  6. &main.Config{a:[]int{513, 514, 515, 516, 517, 518}} 
  7. &main.Config{a:[]int{513, 514, 515, 516, 517, 518}} 
  8. &main.Config{a:[]int{514, 515, 516, 517, 518, 519}} 
  9. [...] 

這次達(dá)到了我們的預(yù)期并且也沒有產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)。

使用 atomic 解決數(shù)據(jù)競(jìng)爭(zhēng) 

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "sync" 
  6.     "sync/atomic" 
  7.  
  8. type Config struct { 
  9.     a []int 
  10.  
  11. func main() { 
  12.     var v atomic.Value 
  13.  
  14.     // 寫入數(shù)據(jù) 
  15.     go func() { 
  16.         var i int 
  17.         for { 
  18.             i++ 
  19.             cfg := Config{ 
  20.                 a: []int{i, i + 1, i + 2, i + 3, i + 4, i + 5}, 
  21.             } 
  22.             v.Store(cfg) 
  23.         } 
  24.     }() 
  25.  
  26.     // 讀取數(shù)據(jù) 
  27.     var wg sync.WaitGroup 
  28.     for n := 0; n < 4; n++ { 
  29.         wg.Add(1) 
  30.         go func() { 
  31.             for n := 0; n < 100; n++ { 
  32.                 cfg := v.Load() 
  33.                 fmt.Printf("%#v\n", cfg) 
  34.             } 
  35.             wg.Done() 
  36.         }() 
  37.     } 
  38.  
  39.     wg.Wait() 

這里我們使用了 atomic 包,通過運(yùn)行我們發(fā)現(xiàn),也同樣達(dá)到了我們期望的結(jié)果: 

  1. [...] 
  2. main.Config{a:[]int{219142, 219143, 219144, 219145, 219146, 219147}} 
  3. main.Config{a:[]int{219491, 219492, 219493, 219494, 219495, 219496}} 
  4. main.Config{a:[]int{219826, 219827, 219828, 219829, 219830, 219831}} 
  5. main.Config{a:[]int{219948, 219949, 219950, 219951, 219952, 219953}} 

從生成的輸出結(jié)果而言,看起來使用 atomic 包的解決方案要快得多,因?yàn)樗梢陨筛叩臄?shù)字序列。為了更加嚴(yán)謹(jǐn)?shù)淖C明這個(gè)結(jié)果,我們下面將對(duì)這兩個(gè)程序進(jìn)行基準(zhǔn)測(cè)試。

性能分析

一個(gè) benchmark 應(yīng)該根據(jù)被測(cè)量的內(nèi)容來解釋。因此,我們假設(shè)之前的程序,有一個(gè)不斷存儲(chǔ)新配置的 數(shù)據(jù)寫入器,同時(shí)也有多個(gè)不斷讀取配置的 數(shù)據(jù)讀取器。為了涵蓋更多潛在的場(chǎng)景,我們還將包括一個(gè)只有 數(shù)據(jù)讀取器 的 benchmark,假設(shè) Config 不經(jīng)常改變。

下面是部分 benchmark 的代碼: 

  1. func BenchmarkMutexMultipleReaders(b *testing.B) { 
  2.     var lastValue uint64 
  3.     var mux sync.RWMutex 
  4.     var wg sync.WaitGroup 
  5.  
  6.     cfg := Config{ 
  7.         a: []int{0, 0, 0, 0, 0, 0}, 
  8.     } 
  9.  
  10.     for n := 0; n < 4; n++ { 
  11.         wg.Add(1) 
  12.  
  13.         go func() { 
  14.             for n := 0; n < b.N; n++ { 
  15.                 mux.RLock() 
  16.                 atomic.SwapUint64(&lastValue, uint64(cfg.a[0])) 
  17.                 mux.RUnlock() 
  18.             } 
  19.             wg.Done() 
  20.         }() 
  21.     } 
  22.  
  23.     wg.Wait() 

執(zhí)行上面的測(cè)試代碼后我們可以得到如下的結(jié)果: 

  1. name                              time/op 
  2. AtomicOneWriterMultipleReaders-4  72.2ns ± 2% 
  3. AtomicMultipleReaders-4           65.8ns ± 2% 
  4.  
  5. MutexOneWriterMultipleReaders-4    717ns ± 3% 
  6. MutexMultipleReaders-4             176ns ± 2% 

基準(zhǔn)測(cè)試證實(shí)了我們之前看到的性能情況。為了了解 mutex 的瓶頸到底在哪里,我們可以在啟用 tracer 的情況下重新運(yùn)行程序。

goroutines 運(yùn)行時(shí)不間斷,能夠完成任務(wù)。對(duì)于帶有 mutex 的程序的配置文件,得到的結(jié)果那是完全不同的。

現(xiàn)在運(yùn)行時(shí)間相當(dāng)零碎,這是由于停放 goroutine 的 mutex 造成的。這一點(diǎn)可以從 goroutine 的概覽中得到證實(shí),其中顯示了同步時(shí)被阻塞的時(shí)間。

屏蔽時(shí)間大概占到三分之一的時(shí)間,這一點(diǎn)可以從下面的 block profile 的圖中詳細(xì)看到。 

在這種情況下,atomic 包肯定會(huì)帶來優(yōu)勢(shì)。但是,在某些方面可能會(huì)降低性能。例如,如果你要存儲(chǔ)一張大地圖,每次更新地圖時(shí)都要復(fù)制它,這樣效率就很低。

via: https://medium.com/a-journey-with-go/go-how-to-reduce-lock-contention-with-the-atomic-package-ba3b2664b549

作者:Vincent Blanchon 譯者:double12gzh 校對(duì):lxbwolf

責(zé)任編輯:未麗燕 來源: Go語言中文網(wǎng)
相關(guān)推薦

2023-12-20 09:50:53

數(shù)據(jù)庫架構(gòu)

2023-12-01 08:54:50

Java原子類型

2015-03-27 18:01:58

云計(jì)算SaaS應(yīng)用渠道沖突

2023-07-05 08:18:54

Atomic類樂觀鎖悲觀鎖

2017-03-07 09:17:51

AtomicDocker遠(yuǎn)程

2016-10-17 13:33:26

原子主機(jī)AnsibleCockpit

2017-11-16 16:15:28

Await開發(fā)嵌套

2018-09-12 15:38:42

Javaatomic編程

2015-10-23 17:29:24

AtomicOpenStack 應(yīng)用部署

2025-04-03 08:10:00

網(wǎng)絡(luò)IP沖突抓包

2011-03-07 09:05:49

鎖競(jìng)爭(zhēng)MySQL等待時(shí)間

2011-03-15 15:47:04

MySQL鎖競(jìng)爭(zhēng)

2011-04-11 11:32:29

Oracle分區(qū)表磁盤IO沖突

2022-06-23 09:00:00

JavaScriptHTML應(yīng)用程序

2018-05-07 13:42:52

LinuxnpmNodeJS

2022-03-14 18:18:11

橫向攻擊網(wǎng)絡(luò)攻擊

2024-10-07 10:07:31

2024-01-05 16:43:30

數(shù)據(jù)庫線程

2015-07-10 09:08:52

IP地址IP地址沖突

2021-09-03 08:21:20

前端代碼模塊
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)