乐趣区

关于golang:一文完全掌握-Go-mathrand

Go 获取随机数是开发中常常会用到的性能, 不过这个外面还是有一些坑存在的, 本文将齐全分析 Go math/rand, 让你轻松应用 Go Rand.

开篇一问: 你感觉 rand 会 panic 吗 ?

源码分析

math/rand 源码其实很简略, 就两个比拟重要的函数

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
        }
    }
}

这个函数就是在设置 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] = 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) {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 = 48271
        Q = 44488
        R = 3399
    )

    hi := x / Q
    lo := x % Q
    x = A*lo - R*hi
    if x < 0 {x += int32max}
    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) {
    // .. 通过服务发现获取到一堆 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 将来会优化吗? 以及如何优化? 欢送留言探讨

欢送关注公众号: HHFCodeRv 及时关注我的动静

退出移动版