C#とランダム

古くて新しいわけはない昔ながらのSystem.Randomのお話。Randomのコンストラクタは二種類あって、seed引数アリの場合は必ず同じ順序で数値を返すようになります。

// 何度実行しても同じ結果
var rand = new Random(0);
Console.WriteLine(rand.Next()); // 1559595546
Console.WriteLine(rand.Next()); // 1755192844
Console.WriteLine(rand.Next()); // 1649316166

例えばゲームのリプレイなどは、ランダムだけど同一の結果が得られることを期待したいわけなので、大事大事ですね。(とはいえ、Windows-CLIとLinux-monoでは結果が違ったりするので、マルチプラットフォームでの共有などという場合は、別策を取ったほうがよさそうです)。何も渡さない場合はseedとしてEnvironment.TickCountが渡されます。精度はミリ秒。ということは、ですね、例えばループの中でRandomをnewするとですよ、

for (int i = 0; i < 100; i++)
{
    var rand = new Random();
    Console.WriteLine(rand.Next());
}

マシンスペックにもよりますが、私の環境では30個ぐらい同じ数値が出た後に、別の、また30個ぐらい同じ数値が続き……となりました。何故か、というと、seedがEnvironment.TickCountだからで、ループ内といったようなミリ秒を超える超高速の状態で生成されている時は、seed値が同じとなってしまうから。なので、正しくは

var rand = new Random();
for (int i = 0; i < 100; i++)
{
    Console.WriteLine(rand.Next());
}

といったように、ループの外に出す必要性があります。

ランダムなランダム

では、ランダムなランダムが欲しい場合は。例えばマルチスレッド。そうでなくても、例えばループの外に出す(直接的でなくてもメソッドの中身がそうなっていて、意図せず使われてしまう可能性がある)のを忘れてしまうのを強制的に避ける場合。もしくは、別にマルチスレッドは気を付けるよー、といっても、ASP.NETとか複数リクエストが同時に走るわけで、同タイミングでのRandom生成になってしまう可能性は十分にある。そういう時は、RandomNumberGeneratorを使います。

using (var rng = new RNGCryptoServiceProvider())
{
    // 厳密にランダムなInt32を作る
    var buffer = new byte[sizeof(int)];
    rng.GetBytes(buffer);
    var seed = BitConverter.ToInt32(buffer, 0);
    // そのseedを基にRandomを作る
    var rand = new Random(seed);
}

これでマルチスレッドでも安全安心だ!勿論、RNGCryptoServiceProviderはちょっとコスト高。でも、全然我慢できる範囲ではある。お終い。

ThreadLocal

でも、これって別にスレッドセーフなランダムが欲しいってだけなわけだよね、それなのにちょっとした、とはいえ、コスト高を背負うのって馬鹿げてない?そこで出てくるのがThreadLocal<T>、.NET 4.0以降ですが、スレッド単位で一意な変数を宣言できます。それを使った、Jon Skeet氏(ゆーめーじん)の実装

public static class RandomProvider
{    
    private static int seed = Environment.TickCount;
    
    private static ThreadLocal<Random> randomWrapper = new ThreadLocal<Random>(() =>
        new Random(Interlocked.Increment(ref seed))
    );

    public static Random GetThreadRandom()
    {
        return randomWrapper.Value;
    }
}

なるほどねー!これなら軽量だし、とってもセーフで安心できるしイイね!もし複数スレッドで同時タイミングで初期化が走った時のために、Interlocked.Incrementで、必ず違う値がseedになるようになってるので、これなら色々大丈夫。

マルチスレッド→マルチサーバー

けれど、大丈夫なのは、一台のコンピューターで完結する時だけの時の話。クラウドでしょ!サーバー山盛りでしょ!な時代では、サーバーをまたいで同時タイミングなEnvironment.TickCountで初期化されてしまう可能性が微レ存。というわけで、Environment.TickCountに頼るのは完全に安全ではない。じゃあ、そう、合わせ技で行けばいいじゃない、seedは完全ランダムで行きましょう。

public static class RandomProvider
{
    private static ThreadLocal<Random> randomWrapper = new ThreadLocal<Random>(() =>
    {
        using (var rng = new RNGCryptoServiceProvider())
        {
            var buffer = new byte[sizeof(int)];
            rng.GetBytes(buffer);
            var seed = BitConverter.ToInt32(buffer, 0);
            return new Random(seed);
        }
    });

    public static Random GetThreadRandom()
    {
        return randomWrapper.Value;
    }
}

これで、軽量かつ安全安泰なRandomが手に入りました。めでたしめでたし。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive