一文完全掌握 Go math/rand(源码解析)

 更新时间:2021年04月26日 09:05:21   作者:haohongfan  
这篇文章主要介绍了一文完全掌握 Go math/rand(源码解析),本文可以帮助大家快速使用Go Rand.,感兴趣的朋友跟随小编一起看看吧

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 未来会优化吗? 以及如何优化? 欢迎留言讨论

到此这篇关于一文完全掌握 Go math/rand(源码解析)的文章就介绍到这了,更多相关Go math rand内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • golang雪花算法实现64位的ID的示例代码

    golang雪花算法实现64位的ID的示例代码

    本文展示了使用Go语言实现雪花算法生成64位ID的示例代码,雪花算法通过当前时间戳、工作节点ID、数据中心ID和序列号生成唯一的64位ID,确保在分布式系统中的唯一性和时间顺序性,感兴趣的可以了解一下
    2024-09-09
  • Golang urfave/cli库简单应用示例详解

    Golang urfave/cli库简单应用示例详解

    这篇文章主要为大家介绍了Golang urfave/cli库简单应用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • Golang Gin局部和全局中间件使用详解

    Golang Gin局部和全局中间件使用详解

    中间件是放在客户端和服务端的中间,当你的客户端对某个接口发起一个请求,但是在到达接口2之前,这里是有一层中间件的处理。本文详细介绍了Golang Gin局部、全局中间件使用方法,感兴趣的同学可以阅读本文
    2023-04-04
  • viper配置框架的介绍支持zookeeper的读取和监听

    viper配置框架的介绍支持zookeeper的读取和监听

    这篇文章主要介绍了viper配置框架的介绍支持zookeeper的读取和监听,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-05-05
  • Golang中的自定义函数详解

    Golang中的自定义函数详解

    函数构成代码执行的逻辑结构。在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。
    2018-10-10
  • Go web入门Go pongo2模板引擎

    Go web入门Go pongo2模板引擎

    这篇文章主要为大家介绍了Go web编程入门Go pongo2模板引擎使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-05-05
  • mayfly-go部署和使用详解

    mayfly-go部署和使用详解

    这篇文章主要介绍了mayfly-go部署和使用详解,此处部署基于CentOS7.4部署,结合实例代码图文给大家讲解的非常详细,需要的朋友可以参考下
    2022-09-09
  • GoLang编程必备:GoFrame GoLand插件介绍

    GoLang编程必备:GoFrame GoLand插件介绍

    掌握GoLang编程必备工具,我们来深入了解GoFrame GoLand插件,这个小巧但强大的插件将极大提升你的开发效率,让代码流畅如行云流水,一起跟随本指南,解锁更智能的编程之旅!
    2023-12-12
  • golang gorm中格式化时间问题详解

    golang gorm中格式化时间问题详解

    这篇文章主要给大家介绍了关于golang gorm中格式化时间问题的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用golang具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-07-07
  • GoLang中的iface 和 eface 的区别解析

    GoLang中的iface 和 eface 的区别解析

    iface 和 eface 都是 Go 中描述接口的底层结构体,区别在于 iface 描述的接口包含方法,而 eface 则是不包含任何方法的空接口:interface{},这篇文章主要介绍了GoLang之iface 和 eface 的区别,需要的朋友可以参考下
    2022-09-09

最新评论