閉じる

C#でRandomを使い回す正しい方法:インスタンス生成の注意点とパフォーマンス改善

C#でプログラムを開発する際、乱数生成はゲームのロジックやシミュレーション、テストデータの作成など、幅広い場面で必要とされる機能です。

乱数を生成するために最も一般的に使用されるのがSystem.Randomクラスですが、その使い方を誤ると、意図しない動作やパフォーマンスの低下を招く恐れがあります。

特に初心者から中級者にかけて陥りやすいのが、乱数が必要になるたびに新しいインスタンスを生成してしまうという問題です。

本記事では、最新のC#環境において、なぜRandomインスタンスを使い回すべきなのか、そしてどのように実装するのが最適なのかを詳しく解説します。

Randomインスタンスの不適切な生成が引き起こす問題

C#のRandomクラスについて正しく理解するために、まずはよくある誤った実装例とその影響を見ていきましょう。

最も典型的な失敗例は、ループ処理の中でnew Random()を呼び出すことです。

同じ数値が連続して出力される理由

コンピュータにおける乱数は、完全にランダムなものではなく、特定の計算式に基づいた「擬似乱数」です。

この計算の起点となる値を「シード値」と呼びますが、引数なしでnew Random()を呼び出すと、デフォルトで現在のシステム時刻がシード値として使用されます。

近年のコンピュータは非常に高速であるため、短い時間間隔でループ処理が行われると、複数のインスタンスが全く同じシステム時刻をシード値として受け取ってしまいます。

その結果、異なるインスタンスであるにもかかわらず、全く同じ乱数列が生成されるという現象が発生します。

C#
// 誤った実装例:ループ内で毎回インスタンスを生成
for (int i = 0; i < 5; i++)
{
    Random rnd = new Random();
    Console.WriteLine(rnd.Next(1, 101));
}
実行結果
42
42
42
42
42

このように、期待に反して同じ数字が並ぶバグは、乱数生成の仕組みを正しく理解していないことが原因で起こります。

ガベージコレクション(GC)への負荷

パフォーマンスの観点からも、インスタンスを頻繁に生成・破棄することは避けるべきです。

Randomクラスは参照型であるため、インスタンス化のたびにヒープメモリが割り当てられます。

大量のループでインスタンスを生成し続けると、ガベージコレクション(GC)の頻度が高まり、アプリケーション全体のパフォーマンスが低下する要因となります。

特に高頻度で乱数を必要とするアプリケーションでは、このオーバーヘッドが無視できないレベルに達することがあります。

Randomインスタンスを使い回す基本的な方法

前述の問題を解決するための最もシンプルな方法は、単一のRandomインスタンスを作成し、それを使い回すことです。

フィールド変数としての保持

クラスのフィールドとしてRandomインスタンスを保持することで、メソッドが呼ばれるたびに新しいインスタンスを作成する必要がなくなります。

C#
public class RandomGenerator
{
    // クラスのフィールドとして1度だけインスタンス化する
    private readonly Random _random = new Random();

    public int GetNextValue()
    {
        return _random.Next(1, 101);
    }
}

この方法であれば、シード値が固定される心配もなく、メモリ消費も最小限に抑えることができます。

静的(static)フィールドでの共有

複数のクラスや場所で共通の乱数生成器を使用したい場合は、staticフィールドとして定義することも一つの選択肢です。

ただし、staticRandomインスタンスには、後述する「スレッドセーフ」に関する重要な注意点があります。

マルチスレッド環境におけるスレッドセーフの注意点

System.Randomクラスは、設計上「スレッドセーフ」ではありません。

これは、複数のスレッドから同時に同じRandomインスタンスのメソッドを呼び出すと、内部状態が破壊される可能性があることを意味します。

内部状態の破壊が生む問題

もしマルチスレッド環境で一つのRandomインスタンスを共有し、ロック(lock)をかけずにアクセスした場合、どうなるでしょうか。

最悪の場合、内部のビット状態がすべてゼロになり、それ以降の戻り値がすべて「0」に固定されるという致命的な不具合が発生することがあります。

この現象は再現性が低く、デバッグが非常に困難なバグとなるため、設計段階で対策を講じることが不可欠です。

lock文による同期処理

最も古典的な解決策は、lockキーワードを使用して排他制御を行うことです。

C#
public static class SafeRandom
{
    private static readonly Random _globalRandom = new Random();

    public static int Next(int min, int max)
    {
        lock (_globalRandom)
        {
            return _globalRandom.Next(min, max);
        }
    }
}

しかし、この方法はスレッド間の競合が発生した場合に待機時間が発生するため、高並列なシステムではボトルネックとなる可能性があります。

モダンC#における決定版:Random.Shared

.NET 6以降を使用している環境であれば、「Random.Shared」プロパティを使用するのが現在のベストプラクティスです。

これは、.NETランタイムが提供するスレッドセーフで高性能なRandomインスタンスです。

Random.Sharedのメリット

Random.Sharedを使用すると、開発者が自前でインスタンスを管理したり、スレッドセーフを考慮したりする必要がなくなります。

  • インスタンスの生成が不要: プロパティから直接アクセスするだけです。
  • スレッドセーフ: 内部的にスレッドごとに最適な管理が行われているため、競合の心配がありません。
  • パフォーマンス: lock文などの明示的な同期コストを抑えつつ、安全に乱数を取得できます。
C#
// モダンなC#での書き方
int diceRoll = Random.Shared.Next(1, 7);
Console.WriteLine($"サイコロの目: {diceRoll}");

新規の開発プロジェクトであれば、特別な理由がない限り「Random.Shared」を第一選択にすべきです。

パフォーマンスと用途の比較表

これまでに紹介した手法を、状況に応じて使い分けるための比較表を作成しました。

手法推奨環境スレッドセーフメリット
Random.Shared.NET 6以降(汎用)高速・安全・簡潔
インスタンスのフィールド保持単一スレッド・旧.NET×特定のシード値を固定可能
lock + 静的Random旧.NET(マルチスレッド)確実な同期が可能
ThreadLocal<Random>.NET 5以前(高並列)競合なしで最高速

暗号学的に安全な乱数が必要な場合

ここまで紹介したRandomクラスは、あくまで「擬似乱数」を生成するものです。

もしパスワードのハッシュ化、トークンの生成、暗号鍵の作成など、セキュリティに関わる用途で使用する場合は、Randomクラスを使用してはいけません。

擬似乱数はパターンが予測可能であるため、攻撃者によって次の値が推測されるリスクがあるからです。

RandomNumberGeneratorの使用

セキュリティが重視される場面では、System.Security.Cryptography.RandomNumberGeneratorを使用します。

C#
using System.Security.Cryptography;

// セキュアな乱数の生成
byte[] randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
    rng.GetBytes(randomNumber);
}

RandomNumberGeneratorは、OSレベルの予測不可能なエントロピー源を使用するため、暗号学的な安全性が保証されます。

ただし、生成速度は通常のRandomよりも低速であるため、ゲームの演出などの非セキュリティ用途では通常のRandomを使い回すのが適切です。

まとめ

C#における乱数生成は、一見簡単に見えますが、インスタンスの寿命管理を誤ると深刻なバグやパフォーマンス低下の原因となります。

「Randomインスタンスはループ内で生成せず、適切に使い回す」という原則を徹底することが重要です。

現代の.NET開発においては、以下の3点を意識して実装を選定してください。

  • .NET 6以降であれば、常に Random.Shared の利用を検討する。
  • それ以前のバージョンや特別なシード値が必要な場合は、フィールド変数としてインスタンスを保持し、使い回す。
  • セキュリティが関わる用途では、必ず RandomNumberGenerator を選択する。

これらを正しく使い分けることで、バグがなく、効率的で、安全なアプリケーションを構築することができます。

乱数の扱い一つでプログラムの信頼性は大きく変わるため、ぜひこの機会に適切な実装方法をマスターしてください。

URLをコピーしました!