45、随机数:rand()的种子设置与伪随机数周期
说到随机数,很多嵌入式工程师第一反应就是 rand()。嗯,这个函数确实方便,但你真的了解它吗?
我见过不少项目,产品跑着跑着突然出现诡异的“复现”现象——明明应该是随机的事件,却总在固定时间点重复出现。查到最后,问题都出在随机数上。
说白了,rand() 生成的并不是真正的随机数,而是伪随机数。它背后是一个确定的数学公式,只要初始状态(种子)相同,生成的序列就完全一样。
伪随机数的本质
标准 C 库里的 rand() 通常使用线性同余生成器(LCG)。公式长这样:
next = (current * A + C) % M
其中 A、C、M 都是常量。每次调用 rand(),内部状态就更新一次,然后返回一个值。
你想想看,这个状态空间是有限的。M 决定了周期上限。在大多数实现中,RAND_MAX 是 32767,内部状态也是 32 位或 31 位。这意味着什么?
意味着最多跑 2^31 次,序列就会重复。对于桌面程序来说这不算啥,但在嵌入式设备里,如果长时间运行,周期问题就会暴露。
核心要点:伪随机数不是真随机,它是可预测的。种子相同,序列就相同。
种子设置:srand() 的正确用法
我习惯在程序启动时调用一次 srand() 来设置种子。常见的做法是用 time(NULL):
#include <stdlib.h>
#include <time.h>
int main() {
srand((unsigned int)time(NULL));
// 后续使用 rand()
int r = rand();
return 0;
}
但这里有个坑——如果程序在 1 秒内多次启动,time() 返回的值是一样的,种子也就一样。我在项目中遇到过这种情况:设备上电自检时连续调用两次初始化函数,结果两次生成的“随机”序列完全一致。
注意:不要频繁调用 srand()。每次调用都会重置随机数序列,反而会降低随机性。通常只在程序入口处调用一次。
更好的做法是结合硬件信息来生成种子。比如用 ADC 的噪声、时钟抖动、或者芯片的唯一 ID:
// 结合多种信息生成种子
unsigned int seed = (unsigned int)time(NULL);
seed ^= (unsigned int)__get_cycle_count(); // 读取 CPU 周期计数
seed ^= (unsigned int)read_unique_chip_id(); // 读取芯片唯一 ID
srand(seed);
这样做的好处是,即使同一秒启动,种子也会因为硬件差异而不同。
伪随机数周期:你踩过这个坑吗?
我曾经调试过一个通信协议栈,里面用随机数来做退避算法。测试时一切正常,但放到现场跑了三天后,设备开始出现周期性的冲突。查了两天才发现——随机数周期到了。
标准 rand() 的周期通常是 2^31 左右。如果每秒调用 1000 次,大约 24 天就会重复。对于需要长期运行的嵌入式设备,这绝对是个隐患。
怎么解决?
- 使用更好的随机数生成器:比如
rand48系列(周期 2^48),或者 Mersenne Twister(周期 2^19937) - 自己实现 LCG:选择更大的模数 M,比如 2^64
- 混合多个生成器:把两个不同周期的生成器结果做异或
这里给一个简单的 64 位 LCG 实现:
static unsigned long long seed = 1;
void my_srand(unsigned long long s) {
seed = s;
}
int my_rand() {
seed = seed * 6364136223846793005ULL + 1442695040888963407ULL;
return (int)(seed >> 33); // 取高 31 位
}
这个生成器的周期是 2^64,对于绝大多数嵌入式应用来说完全够用了。
随机数的质量评估
不是所有随机数都一样。我见过有人用 rand() % 100 来生成 0-99 的随机数,结果发现低位的随机性很差。为什么?
因为 LCG 的低位比特周期更短。比如 rand() % 2 的结果可能交替出现 0 和 1,根本不是随机的。
| 取模方式 | 低位随机性 | 建议 |
|---|---|---|
rand() % N |
差(N 为 2 的幂时尤其差) | 避免使用 |
rand() / (RAND_MAX / N) |
较好 | 推荐使用 |
(rand() << 15) | rand() |
好 | 需要更大范围时使用 |
我个人习惯用除法而不是取模:
// 生成 0 到 N-1 的随机数
int rand_range(int N) {
return (int)((double)rand() / (RAND_MAX + 1.0) * N);
}
这样能避免低位周期问题,分布也更均匀。
SVG:伪随机数生成与周期示意图
嵌入式环境下的特殊考量
在嵌入式系统里,资源有限,不能随便用复杂的随机数算法。我一般遵循这几个原则:
- 不要用
rand()做安全相关的事情——它太容易被预测了 - 如果只需要简单的随机延迟,用 LCG 就够了,别上 Mersenne Twister
- 注意线程安全——
rand()不是线程安全的,多任务环境要加锁或用线程本地存储 - 考虑功耗——频繁调用随机数生成器会增加 CPU 负载
小技巧:如果芯片有硬件随机数生成器(比如 STM32 的 RNG 外设),优先使用。它基于物理噪声,是真随机,没有周期问题。
避坑指南
我曾经接手过一个项目,代码里到处是 srand(time(NULL)),每次调用 rand() 之前都设一次种子。结果呢?因为 time() 精度是秒,同一秒内所有种子都一样,生成的“随机数”完全一样。
另一个常见错误是用 rand() % 6 模拟骰子。我刚才说了,低位随机性差,结果 0 和 3 出现的概率可能比其他数字高。测试时看不出来,但用户用久了就会发现规律。
嗯,记住一句话:种子只设一次,取模要谨慎,周期要算清。
公众号:蓝海资料掘金营,微信deep3321