一文完全掌握 Go math/rand

Go 获取随机数是开发中经常会用到的功能, 不过这个里面还是有一些坑存在的, 本文将完全剖析 Go math/rand, 让你轻松使用 Go Rand.

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

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 }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpwpgg.html