Golang math/rand 源码剖析避坑指南
文章目录
- 1.前言
- 2.剖析
- 3.核心函数
- 4.需要避开的坑
- 参考文献
1.前言
Go 版本为 go 1.17。
go version go1.17 darwin/amd64
本文以type rand struct
为切入点,看下 Go 伪随机数的实现原理。
// A Rand is a source of random numbers.
type Rand struct {src Sources64 Source64 // non-nil if src is source64// readVal contains remainder of 63-bit integer used for bytes// generation during most recent Read call.// It is saved so next Read call can start where the previous// one finished.readVal int64// readPos indicates the number of low-order bytes of readVal// that are still valid.readPos int8
}
2.剖析
伪随机数如果每次种子相同,那么生成的随机序列也是相同的。下面通过赋予不同的种子创建一个随机数发生器。
r := rand.New(rand.NewSource(time.Now().UnixNano()))r.Intn(62) // 生成 [0, n) 内的整数
我们以 rand.Intn()
为例,看下随机数的实现。
// Intn returns, as an int, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
func (r *Rand) Intn(n int) int {if n <= 0 {panic("invalid argument to Intn")}if n <= 1<<31-1 {return int(r.Int31n(int32(n)))}return int(r.Int63n(int64(n)))
}
我传入的是 62 小于,所以调用的是r.Int31n()
。
// Int31n returns, as an int32, a non-negative pseudo-random number in the half-open interval [0,n).
// It panics if n <= 0.
func (r *Rand) Int31n(n int32) int32 {if n <= 0 {panic("invalid argument to Int31n")}if n&(n-1) == 0 { // n is power of two, can maskreturn r.Int31() & (n - 1)}max := int32((1 << 31) - 1 - (1<<31)%uint32(n))v := r.Int31()for v > max {v = r.Int31()}return v % n
}
因为我们的 n=62 不是 2 的幂,所以走的是下面的逻辑。其中这个 max 操作需要明白其作用。
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
max 就是将 int32 范围 [0, (1 << 31) - 1] 内最后取模不能覆盖 [0, n) 的部分去掉,保证 [0, n) 内各个整数出现的概率相同。看下几个具体的值就明白了。
var n int32 = 62
tail := (1<<31) % uint32(n)
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
fmt.Println(tail) // 2
fmt.Println(max) // 2147483645
fmt.Println(max % n) // 61
再看下真正产生随机数的函数r.Int31()
。
// Int31 returns a non-negative pseudo-random 31-bit integer as an int32.
func (r *Rand) Int31() int32 { return int32(r.Int63() >> 32) }
其又调用的是r.Int63()
,取高 31 位作为 int32 的随机值。
// Int31 returns a non-negative pseudo-random 31-bit integer as an int32.
func (r *Rand) Int31() int32 { return int32(r.Int63() >> 32) }
其又调用的是r.src.Int63()
。我们先看下type Source interface
的定义。
// A Source represents a source of uniformly-distributed
// pseudo-random int64 values in the range [0, 1<<63).
type Source interface {Int63() int64Seed(seed int64)
}
我们初始化 Rand 的时候,通过rand.New(rand.NewSource(seed))
创建,看下rand.New()
的实现。
// New returns a new Rand that uses random values from src
// to generate other random values.
func New(src Source) *Rand {s64, _ := src.(Source64)return &Rand{src: src, s64: s64}
}
可见 Rand 使用的是rand.NewSource()
传入的 Source,看下rand.NewSource()
的实现。
// NewSource returns a new pseudo-random Source seeded with the given value.
// Unlike the default Source used by top-level functions, this source is not
// safe for concurrent use by multiple goroutines.
func NewSource(seed int64) Source {var rng rngSourcerng.Seed(seed)return &rng
}
可见 Source 的实际类型是 rngSource,实现了接口 Source,其定义如下:
type rngSource struct {tap int // index into vecfeed int // index into vecvec [rngLen]int64 // current feedback register
}
我们再看下rngSource.Int63()
的实现。
// Int63 returns a non-negative pseudo-random 63-bit integer as an int64.
func (rng *rngSource) Int63() int64 {return int64(rng.Uint64() & rngMask)
}
其中 rngMask 的定义如下,表示 Int64 的最大值,作用是作为掩码。
rngMax = 1 << 63
rngMask = rngMax - 1
至此,我们找到了随机数生成的两个核心函数,一个是根据种子初始化数组 rngSource.vec
的函数rngSource.Seed()
,一个是从数组 rngSource.vec
取出随机数的rngSource.Uint64()
。
3.核心函数
我们看下随机数的真正生成函数rngSource.Uint64()
。
// Uint64 returns a non-negative pseudo-random 64-bit integer as an uint64.
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] = xreturn uint64(x)
}
实际上,我们在调用Intn(), Int31n(), Int63(), Int63n()
等其他函数,最终调用到都是函数rngSource.Uint64()
。可以看到每次调用就是利用 rng.feed, rng.tap 从 rng.vec 中取到两个值相加的结果返回,同时这个结果又重新放入 rng.vec。这么做的目的显而易见,让随机数更加丰富随机,而不是仅局限于 rng.vec 数组中的值。
另外 rng.tap、rng.feed 和 rng.vec 的初始化工作是在函数rngSource.Seed()
中完成的。
// Seed uses the provided seed value to initialize the generator to a deterministic state.
func (rng *rngSource) Seed(seed int64) {rng.tap = 0rng.feed = rngLen - rngTapseed = seed % int32maxif seed < 0 {seed += int32max}if seed == 0 {seed = 89482311}x := int32(seed)for i := -20; i < rngLen; i++ {x = seedrand(x)if i >= 0 {var u int64u = int64(x) << 40x = seedrand(x)u ^= int64(x) << 20x = seedrand(x)u ^= int64(x)u ^= rngCooked[i]rng.vec[i] = u}}
}
相关常量定义如下。
const (rngLen = 607rngTap = 273rngMax = 1 << 63rngMask = rngMax - 1int32max = (1 << 31) - 1
)
其中函数seedrand()
也需要关注下,其是完成对 seed 的变换。这也导致了相同的 seed, 最终设置到 rng.vec 里面的值是相同的,通过rngSource.Uint64()
取出的也是相同的值。
// seed rng x[n+1] = 48271 * x[n] mod (2**31 - 1)
func seedrand(x int32) int32 {const (A = 48271Q = 44488R = 3399)hi := x / Qlo := x % Qx = A*lo - R*hiif x < 0 {x += int32max}return x
}
至此我们大值了解了 math/rand 的随机数的生成过程。
4.需要避开的坑
通过上面对 math/rand 的分析,我们应该知道使用时需要避开的坑。
(1)相同种子,每次运行的结果是一样的。 因为随机数是从 rng.vec 数组中取出来的,这个数组是根据种子生成的,相同的种子生成的 rng.vec 数组是相同的。
(2)不同种子,每次运行的结果可能一样。 因为根据种子生成 rng.vec 数组时会有一个取模的操作,模后的结果可能相同,导致 rng.vec 数组相同。
(3)rand.New 初始化出来的 rand 不是并发安全的。 因为每次利用 rng.feed, rng.tap 从 rng.vec 中取到随机值后会将随机值重新放入 rng.vec。如果想并发安全,可以使用全局的随机数发生器 rand.globalRand。
(4)不同种子,随机序列发生碰撞的概率高于单个碰撞概率的乘积。 这是因为存在生日问题。
比如我要随机从数字字母集(62个字符)中获取长度为 6 的邀请码,种子使用用户ID,如果生成 100W 个邀请码,假设前 100W 一个都不重复,那么下一个重复的概率是((1/62)^6 * 100W)≈1/5.6W
,冲突率已经到了在万分之一的概率,远大于想象中的(1/62)^6
。
参考文献
CSDN.一文完全掌握 Go math/rand
CSDN.记录使用 Golang math/rand 随机数遇到的坑
Golang math/rand 源码剖析避坑指南相关推荐
- 《GDAL源码剖析与开发指南》一一1.9 简单的调用
本节书摘来自异步社区出版社<GDAL源码剖析与开发指南>一书中的第1章,第1.9节,作者:李民录 更多章节内容可以访问云栖社区"异步社区"公众号查看. 1.9 简单的调 ...
- java实现gdal栅格矢量化,《GDAL源码剖析与开发指南》一一1.5 GDAL源码目录
本节书摘来自异步社区出版社<GDAL源码剖析与开发指南>一书中的第1章,第1.5节,作者:李民录 更多章节内容可以访问云栖社区"异步社区"公众号查看. 1.5 GDAL ...
- 《GDAL源码剖析与开发指南》导读
前言 GDAL源码剖析与开发指南 GDAL全称是Geospatial Data Abstraction Library(地理空间数据抽象库),是一个在X/MIT许可协议下读写空间数据(包括栅格数据和矢 ...
- Ubuntu18.04 编译Android 10源码 并烧录源码到pixel3的避坑指南
Ubuntu18.04 编译Android 10源码 并烧录源码到pixel3的避坑指南 实验环境 下载Android源码树 在pixel3上安装手机驱动版本 编译Android源码 Android ...
- mac下编译android源码避坑指南(新)
截至目前mac环境下android源码编译最新避坑指南 避坑方法 配置(不说配置的都是耍流氓) 下载 编译 烧录 注意事项 避坑方法 源码.SDK.机型版本一定要清楚,有些特殊的版本需要特殊的方法,官 ...
- 安卓源码避坑指南3——拨打电话的SIM卡无效导致蓝牙断开连接
安卓源码避坑指南3--拨打电话的SIM卡无效导致蓝牙断连 它来了.它来了,它带着BUG赶来了,欢迎大家查看本期的安卓源码避坑指南.本期的问题场景比较特殊,电话SIM卡是无效的(欠费过期了,很是贫穷-) ...
- 4.2.10 Kafka源码剖析, 阅读环境搭建, broker启动流程, topic创建流程, Producer生产者流程, Consumer消费者流程,
目录 4.1 Kafka源码剖析之源码阅读环境搭建 4.1.1 安装配置Gradle 4.1.2 Scala的安装和配置 4.1.3 Idea配置 4.1.4 源码操作 4.2 Kafka源码剖析之B ...
- ThreadLocal源码剖析
目录 一.ThreadLocal 1.1源码注释 1.2 源码剖析 散列算法-魔数0x61c88647 set操作 get操作 remove操作 1.3 功能测试 1.4 应用场景 二.变量可继承的T ...
- 【Java集合源码剖析】Hashtable源码剖析
转载请注明出处:http://blog.csdn.net/ns_code/article/details/36191279 Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元 ...
最新文章
- 高逼格UILabel的闪烁动画效果
- 基于VMware vSphere 5.0的服务器虚拟化实践(9)
- 蓝桥杯-十六进制转八进制(java)
- 三十五、深入Python的引用计数
- 【转】 Linux内核中读写文件数据的方法--不错
- [Hadoop in China 2011] 华为 - NoSQL/NewSQL在传统IT产业的机遇和挑战
- c语言中如何防止输入的格式存在错误,C语言如何避免输入
- 豆瓣再被约谈处罚150万!一年被罚20次,豆瓣到底怎么了?
- Spring Boot使用自定义的properties
- java应用uploadify 3.2丢失session
- css span 右端对齐_使用 CSS 实现具有方面感知的幽灵按钮
- mysql 伪哈希_MySQL技巧--伪哈希索引
- 【Java NIO的深入研究2】RandomAccessFile的使用
- Java项目:医院门诊收费管理系统(java+html+jdbc+mysql)
- 拉普拉斯方程(Laplace's equation)-- 更新中
- Win10相机打不开提示:我们找不到你的相机 错误代码0xa00f4244!
- C#,数值计算,矩阵的行列式(Determinant)、伴随矩阵(Adjoint)与逆矩阵(Inverse)的算法与源代码
- 中国旅行包行业市场供需与战略研究报告
- android锁屏密码文件夹,深入理解Android M 锁屏密码存储方式
- 车牌识别关键技术-车牌定位
热门文章
- 中秋逢国庆 | 盛世华诞 阖家团圆
- 奇怪,勒索软件Ryuk 新版本把这些 *UNIX 文件夹列入了黑名单
- 这个 WebKit 漏洞助力 Pwn2Own 冠军斩获5.5万美元赏金(详细分析)
- HTTP2.0,HTTP1.1,HTTP1.0三者在通性性能上的优化方法
- 计算机网络知识点回顾
- ExtJS4.2学习(11)——高级组件之Grid
- 不能因技术后天的死 而迷茫了今天的“学” 生
- 网上一片红色的中国心,我也来跟随潮流,表达对祖国的热爱!
- python gzip压缩文件
- python程序封装成exe_如何将python脚本封装成exe程序?