深度解密 Go math/rand
Go 獲取隨機數是開發中經常會用到的功能, 不過這個里面還是有一些坑存在的, 本文將完全剖析 Go math/rand, 讓你輕松使用 Go Rand.
開篇一問: 你覺得 rand 會 panic 嗎 ?
rand panic源碼剖析
math/rand 源碼其實很簡單, 就兩個比較重要的函數:
func?(rng?*rngSource)?Seed(seed?int64)?{rng.tap?=?0rng.feed?=?rngLen?-?rngTap//...x?:=?int32(seed)for?i?:=?-20;?i?<?rngLen;?i++?{x?=?seedrand(x)if?i?>=?0?{var?u?int64u?=?int64(x)?<<?40x?=?seedrand(x)u?^=?int64(x)?<<?20x?=?seedrand(x)u?^=?int64(x)u?^=?rngCooked[i]rng.vec[i]?=?u}} }這個函數就是在設置 seed, 其實就是對 rng.vec 各個位置設置對應的值. rng.vec 的大小是 607.
func?(rng?*rngSource)?Uint64()?uint64?{rng.tap--if?rng.tap?<?0?{rng.tap?+=?rngLen}rng.feed--if?rng.feed?<?0?{rng.feed?+=?rngLen}x?:=?rng.vec[rng.feed]?+?rng.vec[rng.tap]rng.vec[rng.feed]?=?xreturn?uint64(x) }我們在使用不管調用 Intn(), Int31n() 等其他函數, 最終調用到就是這個函數. 可以看到每次調用就是利用 rng.feed rng.tap 從 rng.vec 中取到兩個值相加的結果返回了. 同時還是這個結果又重新放入 rng.vec.
在這里需要注意使用 rng.go 的 rngSource 時, 由于 rng.vec 在獲取隨機數時會同時設置 rng.vec 的值, 當多 goroutine 同時調用時就會有數據競爭問題. math/rand 采用在調用 rngSource 時加鎖 ?sync.Mutex 解決.
func?(r?*lockedSource)?Uint64()?(n?uint64)?{r.lk.Lock()n?=?r.src.Uint64()r.lk.Unlock()return }另外我們能直接使用rand.Seed(),rand.Intn(100), 是因為 math/rand 初始化了一個全局的 globalRand 變量.
var?globalRand?=?New(&lockedSource{src:?NewSource(1).(*rngSource)})func?Seed(seed?int64)?{?globalRand.Seed(seed)?}func?Uint32()?uint32?{?return?globalRand.Uint32()?}需要注意到由于調用 rngSource 加了鎖, 所以直接使用rand.Int32()會導致全局的 goroutine 鎖競爭, 所以在高并發場景時, 當你的程序的性能是卡在這里的話, 你需要考慮利用New(&lockedSource{src: NewSource(1).(*rngSource)})為不同的模塊生成單獨的 rand. 不過根據目前的實踐來看, 使用全局的 globalRand 鎖競爭并沒有我們想象中那么激烈. 使用 New 生成新的 rand 里面是有坑的, 開篇的 panic 就是這么產生的, 后面具體再說.
種子(seed)到底起什么作用 ?
func?main()?{for?i?:=?0;?i?<?10;?i++?{fmt.Printf("current:%d\n",?time.Now().Unix())rand.Seed(time.Now().Unix())fmt.Println(rand.Intn(100))} }結果:
current:1613814632 65 current:1613814632 65 current:1613814632 65 ...這個例子能得出一個結論: 相同種子, 每次運行的結果都是一樣的. 這是為什么呢?
在使用 math/rand 的時候, 一定需要通過調用 rand.Seed 來設置種子, 其實就是給 rng.vec 的 607 個槽設置對應的值. 通過上面的源碼那可以看出來, rand.Seed 會調用一個 seedrand 的函數, 來計算對應槽的值.
func?seedrand(x?int32)?int32?{const?(A?=?48271Q?=?44488R?=?3399)hi?:=?x?/?Qlo?:=?x?%?Qx?=?A*lo?-?R*hiif?x?<?0?{x?+=?int32max}return?x }這個函數的計算結果并不是隨機的, 而是根據 seed 實際算出來的. 另外這個函數并不是隨便寫的, 是有相關的數學證明的.
這也導致了相同的 seed, 最終設置到 rng.vec里面的值是相同的, 通過 Intn 取出的也是相同的值.
我遇到的那些坑
1. rand panic
文章開頭的截圖就是項目開發中使用別人封裝的底層庫, 在某天出現的 panic. 大概實現的代碼:
//?random.govar?(rrRand?=?rand.New(rand.NewSource(time.Now().Unix())) )type?Random?struct{}func?(r?*Random)?Balance(sf?*service.Service)?([]string,?error)?{//?..?通過服務發現獲取到一堆ip+port,?然后隨機拿到其中的一些ip和port出來randIndexes?:=?rrRand.Perm(randMax)//?返回這些ip?和port }這個 Random 會被并發調用, 由于 rrRand 不是并發安全的, 所以就導致了調用 rrRand.Perm 時偶爾會出現 panic 情況.
在使用 math/rand 的時候, 有些人使用 math.Intn(), 看了下注釋發現是全局共享了一個鎖, 擔心出現鎖競爭, 所以用 rand.New 來初始化一個新的 rand, 但是要注意到 rand.New 初始化出來的 rand 并不是并發安全的.
修復方案: 就是把 rrRand 換成了 globalRand, 在線上高并發場景下, 發現全局鎖影響并不大.
2. 獲取的都是同一個機器
流量不均勻同樣也是底層封裝的 rpc 庫, 使用 random 的方式來流量分發, 在線上跑了一段時間后, 流量都路由到一臺機器上了, 導致服務直接宕機. 大概實現代碼:
func?Call(ctx?*gin.Context,?method?string,?service?string,?data?map[string]interface{})?(buf?[]byte,?err?error)?{ins,?err?:=?ral.GetInstance(ctx,?ral.TYPE_HTTP,?service)if?err?!=?nil?{//?錯誤處理}defer?ins.Release()if?b,?e?:=?ins.Request(ctx,?method,?data,?head);?e?==?nil?{//?錯誤處理}//?其他邏輯,?重試等等 }func?GetInstance(ctx?*gin.Context,?modType?string,?name?string)?(*Instance,?error)?{//?其他邏輯..switch?res.Strategy?{case?WITH_RANDOM:if?res.rand?==?nil?{res.rand?=?rand.New(rand.NewSource(time.Now().Unix()))}which?=?res.rand.Intn(res.count)case?其他負載均衡查了}//?返回其中一個ip和port }引起問題的原因: 可以看出來每次請求到來都是利用 GetInstance 來獲取一個 ip 和 port, 如果采用 Random 方式的流量負載均衡, 每次都是重新初始化一個 rand. 我們已經知道當設置相同的種子, 每次運行的結果都是一樣的. 當瞬間流量過大時, 并發請求 GetInstance, 由于那一刻 time.Now().Unix() 的值是一樣的, 這樣就會導致獲取到隨機數都是一樣的, 所以就導致最后獲取到的 ip, port 都是一樣的, 流量都分發到這臺機器上了.
修復方案: 修改成 globalRand 即可.
rand 未來期望
說到這里基本上可以看出來, 為了防止全局鎖競爭問題, 在使用 math/rand 的時候, 首先都會想到自定義 rand, 但是就容易整出來莫名其妙的問題.
為什么 math/rand 需要加鎖呢?
大家都知道 math/rand 是偽隨機的, 但是在設置完 seed 后, rng.vec 數組的值基本上就確定下來了, 這明顯就不是隨機了, 為了增加隨機性, 通過 Uint64() 獲取到隨機數后, 還會重新去設置 rng.vec. 由于存在并發獲取隨機數的需求, 也就有了并發設置 rng.vec 的值, 所以需要對 rng.vec 加鎖保護.
使用 rand.Intn() 確實會有全局鎖競爭問題, 你覺得 math/rand 未來會優化嗎? 以及如何優化? 歡迎留言討論.
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的深度解密 Go math/rand的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 跟着邓神 3 天掌握 Go 语言基础(免
- 下一篇: 曹大带我学 Go(6)—— 技术之外