“v2 解决了全局随机数的锁竞争”是常见的说法,但我们实测发现 v1 的顶层函数在默认设置下早就不争锁了。这篇用实验把真正的差异找出来:Seed 的变化、生成器的创建成本、可重复性和并发安全。
G=1 个 goroutine 共 1600 万次:math/rand 的 rand.Intn(100) 8.2 ns/次;math/rand/v2 的 rand.IntN(100) 5.7 ns/次 G=8 个 goroutine 共 1600 万次:math/rand 的 rand.Intn(100) 1.4 ns/次;math/rand/v2 的 rand.IntN(100) 1.0 ns/次 调用 rand.Seed(42) 之后(GODEBUG=""),G=1:math/rand 的 rand.Intn(100) 7.6 ns/次 调用 rand.Seed(42) 之后(GODEBUG=""),G=8:math/rand 的 rand.Intn(100) 1.4 ns/次 调用 rand.Seed(42) 之后(GODEBUG="randseednop=0"),G=1:math/rand 的 rand.Intn(100) 11.6 ns/次 调用 rand.Seed(42) 之后(GODEBUG="randseednop=0"),G=8:math/rand 的 rand.Intn(100) 106.3 ns/次
默认情况下,8 个 goroutine 共 1600 万次调用,v1 的 rand.Intn 是 1.4 ns/次,v2 的 rand.IntN 是 1.0 ns/次——差别很小。只有退回旧行为(GODEBUG=randseednop=0 让 rand.Seed 重新生效)后,全局源带锁,8 个 goroutine 下涨到 106.3 ns,约慢 76 倍。所以现在不用 Seed,v1 的顶层函数也没有锁竞争问题。
Uint64:PCG 4 ns;ChaCha8 4 ns IntN(100)(各自独占一个 *Rand):v1 NewSource 6 ns;v2 PCG 4 ns;v2 ChaCha8 5 ns 新建一个带种子的生成器并取一个数:v1 NewSource 10369 ns、5376 B、1 次分配;v2 PCG 7 ns、16 B、1 次分配;v2 ChaCha8 118 ns、320 B、1 次分配
取一个数两者都只要 4–6 ns。但新建一个带种子的 rand.New(rand.NewSource(seed)) 要 10369 ns、5376 字节,v2 的 NewPCG 只要 7 ns、16 字节(NewChaCha8 118 ns、320 字节)。需要为每个请求/任务各建一个带种子生成器时,v2 便宜得多。
PCG(1,2) 的前 5 个 IntN(1000):769 616 784 796 234 v1 NewSource(1) 的前 5 个 Intn(1000):81 887 847 59 81 v2 顶层 rand.IntN(1000) 本进程的前 5 个:457 453 242 98 514 再运行一次同一个程序: v2 顶层 rand.IntN(1000) 本进程的前 5 个:214 575 992 221 45 前两行(固定种子)两次运行是否完全相同:True
固定种子的生成器两次运行结果完全相同(v1 的 NewSource(1) 和 v2 的 NewPCG(1,2) 都试过);v2 的顶层函数没有设种子的办法,每次运行都不同。需要可复现就自己 New 一个。
共用一个 *Rand:退出码 66;出现 DATA RACE 5 次 共用顶层函数:退出码 0;出现 DATA RACE 0 次;4 个 goroutine 共用顶层函数 rand.IntN:完成
顶层函数并发安全(-race 下 0 次竞争);*rand.Rand 不是,4 个 goroutine 共用一个被报 5 次 DATA RACE。实验里每个 goroutine 用自己的局部变量累加,避免把累加变量的竞争误当成随机数库的问题。
as simulation, but it should not be used for security-sensitive work. Random numbers are generated by a Source, usually wrapped in a Rand. Both types should be used by a single goroutine at a time: sharing among multiple goroutines requires some kind of synchronization. Top-level functions, such as Float64 and Int, are safe for concurrent use by multiple goroutines.
文档说得很直接:Source 和 Rand 由单个 goroutine 使用,顶层函数并发安全,安全敏感的场合不要用这个包。