<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深度解密 Go math/rand

          共 5943字,需瀏覽 12分鐘

           ·

          2021-06-11 18:28

          Go 獲取隨機數(shù)是開發(fā)中經(jīng)常會用到的功能, 不過這個里面還是有一些坑存在的, 本文將完全剖析 Go math/rand, 讓你輕松使用 Go Rand.

          開篇一問: 你覺得 rand 會 panic 嗎 ?

          rand panic

          源碼剖析

          math/rand 源碼其實很簡單, 就兩個比較重要的函數(shù):

          func (rng *rngSource) Seed(seed int64) {
           rng.tap = 0
           rng.feed = rngLen - rngTap

           //...
           x := int32(seed)
           for i := -20; i < rngLen; i++ {
            x = seedrand(x)
            if i >= 0 {
             var u int64
             u = int64(x) << 40
             x = seedrand(x)
             u ^= int64(x) << 20
             x = seedrand(x)
             u ^= int64(x)
             u ^= rngCooked[i]
             rng.vec[i] = u
            }
           }
          }

          這個函數(shù)就是在設(shè)置 seed, 其實就是對 rng.vec 各個位置設(shè)置對應(yīng)的值. 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] = x
           return uint64(x)
          }

          我們在使用不管調(diào)用 Intn(), Int31n() 等其他函數(shù), 最終調(diào)用到就是這個函數(shù). 可以看到每次調(diào)用就是利用 rng.feed rng.tap 從 rng.vec 中取到兩個值相加的結(jié)果返回了. 同時還是這個結(jié)果又重新放入 rng.vec.

          在這里需要注意使用 rng.go 的 rngSource 時, 由于 rng.vec 在獲取隨機數(shù)時會同時設(shè)置 rng.vec 的值, 當(dāng)多 goroutine 同時調(diào)用時就會有數(shù)據(jù)競爭問題. math/rand 采用在調(diào)用 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() }

          需要注意到由于調(diào)用 rngSource 加了鎖, 所以直接使用rand.Int32()會導(dǎo)致全局的 goroutine 鎖競爭, 所以在高并發(fā)場景時, 當(dāng)你的程序的性能是卡在這里的話, 你需要考慮利用New(&lockedSource{src: NewSource(1).(*rngSource)})為不同的模塊生成單獨的 rand. 不過根據(jù)目前的實踐來看, 使用全局的 globalRand 鎖競爭并沒有我們想象中那么激烈. 使用 New 生成新的 rand 里面是有坑的, 開篇的 panic 就是這么產(chǎn)生的, 后面具體再說.

          種子(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))
           }
          }

          結(jié)果:

          current:1613814632
          65
          current:1613814632
          65
          current:1613814632
          65
          ...

          這個例子能得出一個結(jié)論: 相同種子, 每次運行的結(jié)果都是一樣的. 這是為什么呢?

          在使用 math/rand 的時候, 一定需要通過調(diào)用 rand.Seed 來設(shè)置種子, 其實就是給 rng.vec 的 607 個槽設(shè)置對應(yīng)的值. 通過上面的源碼那可以看出來, rand.Seed 會調(diào)用一個 seedrand 的函數(shù), 來計算對應(yīng)槽的值.

          func seedrand(x int32) int32 {
           const (
            A = 48271
            Q = 44488
            R = 3399
           )

           hi := x / Q
           lo := x % Q
           x = A*lo - R*hi
           if x < 0 {
            x += int32max
           }
           return x
          }

          這個函數(shù)的計算結(jié)果并不是隨機的, 而是根據(jù) seed 實際算出來的. 另外這個函數(shù)并不是隨便寫的, 是有相關(guān)的數(shù)學(xué)證明的.

          這也導(dǎo)致了相同的 seed, 最終設(shè)置到 rng.vec里面的值是相同的, 通過 Intn 取出的也是相同的值.

          我遇到的那些坑

          1. rand panic

          文章開頭的截圖就是項目開發(fā)中使用別人封裝的底層庫, 在某天出現(xiàn)的 panic. 大概實現(xiàn)的代碼:

          // random.go

          var (
           rrRand = rand.New(rand.NewSource(time.Now().Unix()))
          )

          type Random struct{}

          func (r *Random) Balance(sf *service.Service) ([]string, error) {
           // .. 通過服務(wù)發(fā)現(xiàn)獲取到一堆ip+port, 然后隨機拿到其中的一些ip和port出來
           randIndexes := rrRand.Perm(randMax)

           // 返回這些ip 和port
          }

          這個 Random 會被并發(fā)調(diào)用, 由于 rrRand 不是并發(fā)安全的, 所以就導(dǎo)致了調(diào)用 rrRand.Perm 時偶爾會出現(xiàn) panic 情況.

          在使用 math/rand 的時候, 有些人使用 math.Intn(), 看了下注釋發(fā)現(xiàn)是全局共享了一個鎖, 擔(dān)心出現(xiàn)鎖競爭, 所以用 rand.New 來初始化一個新的 rand, 但是要注意到 rand.New 初始化出來的 rand 并不是并發(fā)安全的.

          修復(fù)方案: 就是把 rrRand 換成了 globalRand, 在線上高并發(fā)場景下, 發(fā)現(xiàn)全局鎖影響并不大.

          2. 獲取的都是同一個機器

          流量不均勻

          同樣也是底層封裝的 rpc 庫, 使用 random 的方式來流量分發(fā), 在線上跑了一段時間后, 流量都路由到一臺機器上了, 導(dǎo)致服務(wù)直接宕機. 大概實現(xiàn)代碼:

          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. 我們已經(jīng)知道當(dāng)設(shè)置相同的種子, 每次運行的結(jié)果都是一樣的. 當(dāng)瞬間流量過大時, 并發(fā)請求 GetInstance, 由于那一刻 time.Now().Unix() 的值是一樣的, 這樣就會導(dǎo)致獲取到隨機數(shù)都是一樣的, 所以就導(dǎo)致最后獲取到的 ip, port 都是一樣的, 流量都分發(fā)到這臺機器上了.

          修復(fù)方案: 修改成 globalRand 即可.

          rand 未來期望

          說到這里基本上可以看出來, 為了防止全局鎖競爭問題, 在使用 math/rand 的時候, 首先都會想到自定義 rand, 但是就容易整出來莫名其妙的問題.

          為什么 math/rand 需要加鎖呢?

          大家都知道 math/rand 是偽隨機的, 但是在設(shè)置完 seed 后, rng.vec 數(shù)組的值基本上就確定下來了, 這明顯就不是隨機了, 為了增加隨機性, 通過 Uint64() 獲取到隨機數(shù)后, 還會重新去設(shè)置 rng.vec. 由于存在并發(fā)獲取隨機數(shù)的需求, 也就有了并發(fā)設(shè)置 rng.vec 的值, 所以需要對 rng.vec 加鎖保護.

          使用 rand.Intn() 確實會有全局鎖競爭問題, 你覺得 math/rand 未來會優(yōu)化嗎? 以及如何優(yōu)化? 歡迎留言討論.

          瀏覽 42
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩嫩穴 | 精品国内久 | 欧美尻屄| 免费观看黄色一级片 | 天堂网2016 |