C++ 随机数生成的陷阱

从问题出发

事情的来源是如下一小段代码,其基本设想是在服务发现的节点中随机选择几个作为本地的缓存

cpp 复制代码
// get endpoints by service discovery
auto endpoints = XXXDiscovery();

// shffule 
std::shuffle(endpoints.begin(), endpoints.end(), std::default_random_engine());

// get batch_size as local cache
for (int i = 0; i < batch_size; ++i) {
    result[i].ip = endpoints[i].ip;
    result[i].port = endpoints[i].port;
}

return result;

在测试阶段发现若干进程通过这段逻辑发现的下游节点都是一样的,明显不符合预期。

cppreference中找到这样一句描述:

default_random_engine(C++11) an implementation-defined RandomNumberEngine type

通过一段代码进行验证

cpp 复制代码
#include <iostream>
#include <random>

int main() {
    {   
        std::cout << "default construct: ";
        std::default_random_engine engine;
        for (int i = 0; i < 5; ++i) {
            std::cout << engine() << " ";
        }
        std::cout << std::endl;
    }

    {   
        std::cout << "default construct: ";
        std::default_random_engine engine;
        for (int i = 0; i < 5; ++i) {
            std::cout << engine() << " ";
        }
        std::cout << std::endl;
    }

    {   
        std::cout << "construct with seed 42: ";
        std::default_random_engine engine(42);
        for (int i = 0; i < 5; ++i) {
            std::cout << engine() << " ";
        }
        std::cout << std::endl;
    }

    {   
        std::cout << "construct with seed 132: ";
        std::default_random_engine engine(132);
        for (int i = 0; i < 5; ++i) {
            std::cout << engine() << " ";
        }
        std::cout << std::endl;
    }

    return 0;
}

得到的结果如下

shell 复制代码
default construct: 16807 282475249 1622650073 984943658 1144108930 
default construct: 16807 282475249 1622650073 984943658 1144108930 
construct with seed 42: 705894 1126542223 1579310009 565444343 807934826 
construct with seed 132: 2218524 779510869 1588928583 1163544036 698523470 
  • 创建 std::default_random_engine 对象 engine,使用默认构造函数,它会使用实现定义的默认种子。
  • 如果每次使用默认构造函数创建 std::default_random_engine 对象,可能会得到相同的随机数序列(取决于实现)。为了每次运行程序得到不同的随机数序列,可以使用 std::random_device 作为种子

更多关于随机数生成的陷阱

未初始化随机数种子

随机数生成函数 rand() 依赖于一个种子值来初始化随机数序列。如果种子值固定或者没有正确初始化,每次程序运行时生成的随机数序列都会相同,这就失去了随机性。例如,若使用 srand(0) 或者不调用 srand() 函数(此时随机数种子会被设置为1),程序每次运行都会生成相同的随机数序列。

cpp 复制代码
#include <iostream>
#include <cstdlib>

int main() {
    for (int i = 0; i < 5; ++i) {
        std::cout << rand() << std::endl;
    }
    return 0;
}

通常的做法是使用当前时间作为种子值,通过 time(0) 函数获取当前时间:

cpp 复制代码
#include <iostream>
#include <cstdlib>
#include <ctime>

int main() {
    srand(static_cast<unsigned int>(time(0)));

    for (int i = 0; i < 5; ++i) {
        std::cout << rand() << std::endl;
    }
    return 0;
}

rand() 函数基于线性同余算法,其随机范围通常较小(RAND_MAX 一般为 32767),周期较短,生成的随机数序列规律性较强。这使得生成的随机数在某些对随机性要求较高的场景下不适用,例如密码学领域。

分布函数使用不当

在使用分布函数时,如果对其边界条件和特性理解不准确,可能会导致生成的随机数不符合预期。例如,std::uniform_int_distribution 是闭区间,若需要生成 [a, b) 范围的随机数,没有进行正确的调整,就会出现问题。

cpp 复制代码
// https://godbolt.org/z/xE3WPY3zd
#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    // 错误使用,未考虑闭区间问题
    std::uniform_int_distribution<> dis(1, 10);
    for (int i = 0; i < 5; ++i) {
        std::cout << dis(gen) << std::endl;
    }
    return 0;
}

一种可能得结果:

shell 复制代码
3
3
2
10
4

正确的做法是:

  1. 将上界设为略小于 b 的值,如 double upper = std::nextafter(10, std::numeric_limits<double>::min()); // 取 b 的前一个可表示数
  2. 或者在生成值后判断是否小于 b,否则重新生成

伪随机数在安全场景下不够安全

伪随机数生成器(PRNG)使用确定的数学算法产生具备良好统计属性的数字序列,但实际上这种数字序列并非具备真正的随机特性。伪随机数生成器通常以一个种子值为起始,每次计算使用当前种子值生成一个输出及一个新种子,这个新种子会被用于下次计算。在安全场景中,如果种子值可以被预测,那么生成的随机数序列也可以被预测,从而导致安全风险。

因此可以使用 std::random_device 获取安全的随机种子,然后结合 std::mt19937 或其他随机数引擎生成随机数。

也可以使用一些更专业、高性能或适用于特定场景的随机数生成库,可满足高精度、高安全性或特殊分布需求:

  • CUDA Curand: 用于 GPU 加速的随机数生成库,提供高性能的随机数生成。
  • PCG(Permuted Congruential Generator): 比标准库的 mt19937 更快,且具有更好的统计特性(如抗碰撞性)
  • Botan: 一个功能强大的密码学库,包含多种随机数生成器和安全工具
  • GSL: GNU 科学库,提供了各种随机数生成器和统计工具

多线程环境下的竞争问题

在多线程环境中,如果多个线程共享同一个随机数引擎(如 std::mt19937 std::linear_congruential_engine 等),会导致线程安全问题,即数据竞争。不同线程可能会同时修改随机数引擎的状态,从而破坏随机数的生成逻辑。

cpp 复制代码
#include <iostream>
#include <random>
#include <thread>
#include <vector>

std::mt19937 gen;   // 不是线程安全的

void threadFunction() {
    std::uniform_int_distribution<> dis(1, 10);
    for (int i = 0; i < 5; ++i) {
        std::cout << std::this_thread::get_id() << ": " << dis(gen) << std::endl;
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(threadFunction);
    }
    for (auto& t : threads) {
        t.join();
    }
    return 0;
}

可以考虑用 thread_local 存储随机数引擎,或使用互斥锁进行保护

一些最佳实践

使用 <random>

更灵活、更强大,更多引擎和分布函数的支持。

cpp 复制代码
#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(1, 10);
    for (int i = 0; i < 5; ++i) {
        std::cout << dis(gen) << std::endl;
    }
    return 0;
}

正确初始化种子

使用 std::random_device 作为种子初始化随机数引擎,它通常基于硬件随机源,能提供更真实的随机性。同时,种子只需要初始化一次,避免在循环中重复初始化。

正确使用分布函数

在使用分布函数时,要明确其边界条件和特性。如果需要特定范围的随机数,要进行正确的调整。

cpp 复制代码
#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());

    double upper = std::nextafter(10, std::numeric_limits<double>::min()); // 取 b 的前一个可表示数
    std::uniform_int_distribution<> dis(1, upper);
    for (int i = 0; i < 5; ++i) {
        std::cout << dis(gen) << std::endl;
    }
    return 0;
}

考虑性能优化

避免在循环中频繁初始化种子,预先生成一批随机数并缓存起来,以提高性能。

加密场景使用安全随机数

在加密场景中,使用加密安全的随机数生成器,如 std::random_device(若系统支持硬件随机源)或操作系统提供的接口(如 Linux 的 /dev/urandom)。

c 复制代码
// https://godbolt.org/z/dK6xYbYbo
#include <iostream>
#include <random>
#include <fstream>

// 从 /dev/urandom 读取随机数
void generateSecureKey() {
    std::ifstream urandom("/dev/urandom", std::ios::binary);
    if (urandom) {
        unsigned char key[16];
        urandom.read(reinterpret_cast<char*>(key), sizeof(key));
        urandom.close();
        std::cout << "Generated secure key: ";
        for (int i = 0; i < sizeof(key); ++i) {
            std::cout << std::hex << static_cast<int>(key[i]);
        }
        std::cout << std::endl;
    }
}

int main() {
    generateSecureKey();
    return 0;
}

// Generated secure key: f558692b6149b4bb445e8d45896e967

多线程环境下的处理

在多线程环境中,为每个线程创建独立的随机数引擎实例,避免线程安全问题。

验证随机数的分布均匀性

对于对随机性要求较高的场景,使用统计测试(如卡方检验)来验证随机数的分布均匀性和独立性。

cpp 复制代码
// https://godbolt.org/z/5oGb6Tq4q
#include <iostream>
#include <random>
#include <vector>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(1, 10);
    std::vector<int> frequency(10, 0);
    for (int i = 0; i < 1000; ++i) {
        int num = dis(gen);
        frequency[num - 1]++;
    }
    for (int i = 0; i < 10; ++i) {
        std::cout << "Number " << i + 1 << " frequency: " << frequency[i] << std::endl;
    }
    return 0;
}
相关推荐
脏脏a24 分钟前
C++入门篇(下)
c++
今麦郎xdu_1 小时前
【数据结构】红黑树
数据结构·c++·算法·stl
思麟呀1 小时前
list的模拟实现和反向迭代器的底层
c语言·数据结构·c++·list
不爱学英文的码字机器1 小时前
操作系统是如何运行的?
服务器·c语言·c++
长沙红胖子Qt1 小时前
live555开发笔记(二):live555创建RTSP服务器源码剖析,创建rtsp服务器的基本流程总结
c++·音视频开发
upsilon1 小时前
c++自增和自减运算符
c++·后端
菲英的学习笔记1 小时前
C++面试题集合(附答案)
java·c++·面试·职场和发展
想睡hhh1 小时前
c++STL——list的使用和模拟实现
开发语言·c++·list
红狐寻道2 小时前
“vcpkg install”失败问题记录
c++·后端