3.6 并行 RNG

原文链接: https://rust-random.github.io/book/guide-parallel.html

理论:多个 RNG

若要在多个工作线程中同时使用随机生成器,则需要使用多个 RNG。几种建议做法:

  1. 在每个工作线程中使用 rng()。它会在每个使用它的线程上自动(惰性且唯一地)播种。
  2. 用 rng()(或另一个主 RNG)为每个工作线程上的自定义 RNG 播种。主要优势是对所用 RNG 更灵活。
  3. 每个工作单元使用一个自定义 RNG,而非每个工作线程。若这些 RNG 以确定性方式播种,则可得到确定性结果。遗憾的是,从主生成器为每个工作单元播种新 RNG 无法并行完成,因此可能较慢。
  4. 使用单一主种子。对每个工作单元,用主种子播种 RNG 并将 RNG 的流设置为工作单元编号。这可能比 (3) 更快且仍保持确定性,但并非所有 RNG 都支持。

注意:不要简单地为工作线程/单元克隆 RNG。克隆会产生与原始 RNG 相同的输出序列。但若随后为每个克隆设置唯一流,则可以使用克隆。

流

哪些 RNG 族支持多流?

  • ChaCha:ChaCha RNG 支持 256 位种子、64 位流和 64 位计数器(每 16 字块),因此支持 264 条流,每条 268 个字。
  • Hc128 是支持 256 位种子的密码学 RNG;可从(例如)较小的 192 位密钥加 64 位流构造该种子。

注意,从较小密钥加流计数器构造种子的上述方法仅建议用于密码学 PRNG,因为较简单的 RNG 在使用两个相近密钥时输出往往相关,且可能需要「看起来随机」的种子才能产生高质量输出。

非密码学 PRNG 仍可能支持多流,但很可能有显著限制(尤其要注意此类 PRNG 的常见建议:消耗量不要超过生成器周期的平方根)。

  • Xoshiro:Xoshiro 族 RNG 支持 jump 和 long_jump 方法,可有效将单个 RNG 的输出划分为多条流。实践中仅对少量流有用,因为要选择第 n 条「流」必须调用 jump n 次。

  • Pcg:这些 RNG 支持用 state 和 stream 参数构造。但请注意,使用相同密钥的多条流往往高度相关,RNG 因此受到批评。见作者自己的评论。

    PCG RNG 还支持 fn advance(delta) 方法,可用于像 Xoshiro 的 jump 那样将单一流划分为多条子流(但更好,因为可指定偏移)。

实践:非确定性多线程

我们使用 Rayon 的并行迭代器,用 map_init 在每个工作线程中初始化 RNG。注意:该 RNG 可能在多个工作单元间复用,工作单元可能以非确定性方式在工作线程间拆分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use rand::distr::{Distribution, Uniform};
use rayon::prelude::*;

static SAMPLES: u64 = 1_000_000;

fn main() {
    let range = Uniform::new(-1.0f64, 1.0).unwrap();

    let in_circle = (0..SAMPLES)
        .into_par_iter()
        .map_init(|| rand::rng(), |rng, _| {
            let a = range.sample(rng);
            let b = range.sample(rng);
            if a * a + b * b <= 1.0 {
                1
            } else {
                0
            }
        })
        .reduce(|| 0usize, |a, b| a + b);

    // 打印接近 3.14159... 的值
    println!(
        "π is approximately {}",
        4. * (in_circle as f64) / (SAMPLES as f64)
    );
}

实践:确定性多线程

我们使用上述方法 (4) 实现确定性结果:从单一种子初始化所有 RNG,但使用多条流。 我们使用 ChaCha8Rng::set_stream 实现这一点。

此外,我们按 BATCH_SIZE 手动批处理多个工作单元。这很重要,因为初始化 RNG 的成本相对于我们的工作单元(生成两个随机样本加一些简单计算)很大。手动批处理也能提升上述非确定性模拟的性能。

(注意:此示例见 https://github.com/rust-random/rand/blob/master/examples/rayon-monte-carlo.rs。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use rand::distr::{Distribution, Uniform};
use rand::{SeedableRng, rngs::ChaCha8Rng};
use rayon::prelude::*;

static SEED: u64 = 0;
static BATCH_SIZE: u64 = 10_000;
static BATCHES: u64 = 1000;

fn main() {
    let range = Uniform::new(-1.0f64, 1.0).unwrap();

    let in_circle = (0..BATCHES)
        .into_par_iter()
        .map(|i| {
            let mut rng = ChaCha8Rng::seed_from_u64(SEED);
            rng.set_stream(i);
            let mut count = 0;
            for _ in 0..BATCH_SIZE {
                let a = range.sample(&mut rng);
                let b = range.sample(&mut rng);
                if a * a + b * b <= 1.0 {
                    count += 1;
                }
            }
            count
        })
        .reduce(|| 0usize, |a, b| a + b);

    // 打印 3.1409052(确定性、可复现的结果)
    println!(
        "π is approximately {}",
        4. * (in_circle as f64) / ((BATCH_SIZE * BATCHES) as f64)
    );
}
最后修改 August 23, 2026: 更新 (499855b16)