bigywater.tech
Go · 100

math/rand/v2:新的生成器和并发安全

“v2 解决了全局随机数的锁竞争”是常见的说法,但我们实测发现 v1 的顶层函数在默认设置下早就不争锁了。这篇用实验把真正的差异找出来:Seed 的变化、生成器的创建成本、可重复性和并发安全。

1.4 vs 106.3 ns
v1 顶层函数在 8 个 goroutine 下:默认与 GODEBUG=randseednop=0 之后
10369 vs 7 ns
新建一个带种子的生成器:v1 NewSource 与 v2 PCG
0 次 vs 5 次
-race 下共用顶层函数与共用一个 *Rand 的 DATA RACE 数

v1 的全局函数也不争锁

实测 · 本机执行输出
  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 使用,顶层函数并发安全,安全敏感的场合不要用这个包。

分步演示

math/rand/v2
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。并发实验:1600 万次调用平均分给 G 个 goroutine,用 close(start) 同时放行,墙钟时间 ÷ 总次数(所以 G=8 的数字是总吞吐,不是单次延迟),每格 3 次取中位数;创建成本用 testing.Benchmark;-race 版本另外编译;读过的文档:go doc math/rand/v2,go doc math/rand.Seed(Go 1.24 起 Seed 是空操作),godebug.md 的 randseednop
    • 没有验证:PCG 与 ChaCha8 的统计质量、v1/v2 在 Linux 上的差别、rand.N 泛型函数、Shuffle 与 Perm 的性能、IntN 的取模偏差处理(没有读源码)、runtime 里全局随机源的实现细节
    京ICP备2026067502号-1京公网安备11010502063487号