go 獲取隨機數是開發中經常會用到的功能, 不過這個裡面還是有一些坑存在的, 本文將完全剖析 go math/rand, 讓你輕鬆使用 go rand.
開篇一問: 你覺得 rand 會 panic 嗎 ?
math/rand 原始碼其實很簡單, 就兩個比較重要的函式
func (rng *rngsource) seed(seed int64)
}}這個函式就是在設定 seed, 其實就是對 rng.vec 各個位置設定對應的值. rng.vec 的大小是 607.
func (rng *rngsource) uint64() uint64
rng.feed--
if rng.feed < 0
x := rng.vec[rng.feed] + rng.vec[rng.tap]
rng.vec[rng.feed] = x
return 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)
另外我們能直接使用rand.seed(),rand.intn(100), 是因為 math/rand 初始化了乙個全域性的 globalrand 變數.
var globalrand = new(&lockedsource)
func seed(seed int64)
func uint32() uint32
需要注意到由於呼叫 rngsource 加了鎖, 所以直接使用rand.int32()會導致全域性的 goroutine 鎖競爭, 所以在高併發場景時, 當你的程式的效能是卡在這裡的話, 你需要考慮利用new(&lockedsource)為不同的模組生成單獨的 rand. 不過根據目前的實踐來看, 使用全域性的 globalrand 鎖競爭並沒有我們想象中那麼激烈. 使用 new 生成新的 rand 裡面是有坑的, 開篇的 panic 就是這麼產生的, 後面具體再說.
func main()
}結果:
current:1613814632
65current:1613814632
65current:1613814632
65.這個例子能得出乙個結論: 相同種子,每次執行的結果都是一樣的. 這是為什麼呢?
在使用 math/rand 的時候, 一定需要通過呼叫 rand.seed 來設定種子, 其實就是給 rng.vec 的 607 個槽設定對應的值. 通過上面的原始碼那可以看出來, rand.seed 會呼叫乙個 seedrand 的函式, 來計算對應槽的值.
func seedrand(x int32) int32
return x
}這個函式的計算結果並不是隨機的, 而是根據 seed 實際算出來的. 另外這個函式並不是隨便寫的, 是有相關的數學證明的.
這也導致了相同的 seed, 最終設定到 rng.vec裡面的值是相同的, 通過 intn 取出的也是相同的值
1. rand panic
文章開頭的截圖就是專案開發中使用別人封裝的底層庫, 在某天出現的 panic. 大概實現的**
// random.go
var (
程式設計客棧rrrand = rand.new(rand.newsource(time.now().unix()))
)type random struct{}
func (r *random) balance(sf *service.service) (string, error)
這個 random 會被併發呼叫, 由於 rrrand 不是併發安全的, 所以就導致了呼叫 rrrand.perm 時偶爾會出現 panic 情況.
在使用 math/rand 的時候, 有些人使用 math.intn() 看了下注釋發現是全域性共享了乙個鎖, 擔心出現鎖競爭, 所以用 rand.new 來初始化乙個新的 rand, 但是要注意到 rand.new 初始化出來的 rand 並不是併發安全的.
修復方案: 就是把 rrrand 換成了 globalrand, **上程式設計客棧高併發場景下, 發現全域性鎖影響並不大.
2. 獲取的都是同乙個機器
同樣也是底層封裝的 rpc 庫, 使用 random 的方式來流量分發, **上跑了一段時間後, 流量都路由到一台機器上了, 導致服務直接宕機. 大概實現**:
f程式設計客棧unc call(ctx *gin.context, method string, service string, data map[string]inte***ce{}) (buf byte, err error)
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)
which = res.rand.程式設計客棧intn(res.count)
case 其他負載均衡查了
} // 返回其中乙個ip和port
}引起問題的原因: 可以看出來每次請求到來都是利用 getinstance 來獲取乙個 ip 和 port, 如果採用 random 方式的流量負載均衡, 每次都是重新初始化乙個 rand. 我們已經知道當設定相同的種子,每次執行的結果都是一樣的. 當瞬間流量過大時, 併發請求 getinstance, 由於那一刻 time.now().unix() 的值是一樣的, 這樣就會導致獲取到隨機數都是一樣的, 所以就導致最後獲取到的 ip, port都是一樣的, 流量都分發到這台機器上了.
修復方案: 修改成 globalrand 即可.
說到這裡基本上可以看出來, 為了防止全域性鎖競爭問題, 在使用 math/rand 的時候, 首先都會想到自定義 rand, 但是就容易整出來莫名其妙的問題.
為什麼 math/rand 需要加鎖呢?
大家都知道 math/rand 是偽隨機的, 但是在設定完 seed 後, rng.vec 陣列的值基本上就確定下來了, 這明顯就不是隨機了, 為了增加隨機性, 通過 uint64() 獲取到隨機數後, 還會重新去設定 rng.vec. 由於存在併發獲取隨機數的需求, 也就有了併發設定 rng.vec 的值, 所以需要對 rng.vec 加鎖保護.
使用 rand.intn() 確實會有全域性鎖競爭問題, 你覺得 math/rand 未來會優化嗎? 以及如何優化? 歡迎留言討論
一文掌握 TensorFlow 基礎
在後面的課程中我們將主要使用 tensorflow 來實現各種模型的應用,所以在本節我們先來看一下 tensorflow 的基礎知識點。tensorflow 是乙個深度學習庫,由 google 開源,可以對定義在 tensor 張量 上的函式自動求導。tensor 張量 意味著 n 維陣列,flow...
必備!一文掌握Wordpress外掛程式
什麼是外掛程式?wordpress是乙個非常強大的建站系統,而在我們建站的過程中,外掛程式的使用必不可少。外掛程式是wordpress功能的擴充套件,也是wordpress得以獨步天下的 殺手鐗 其外掛程式實現了名副其實的 即插即用 全球有超過100萬的wordpress外掛程式,涵蓋電商 表單 郵...
一文帶你掌握Redis操作指南
摘要 redis是一種支援key value等多種資料結構的儲存系統。redis是一種支援key value等多種資料結構的儲存系統。可用於快取,事件發布或訂閱,高速佇列等場景。該資料庫使用ansi c語言編寫,支援網路,提供字串,雜湊,列表,佇列,集合結構直接訪問,基於記憶體,可持久化。redis...