1. taiga_takahari

    Posted

    taiga_takahari
Changes in title
+Randomクラスの仕様が.NET Core 2.0で変わっていた話
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,142 @@
+C#で乱数を取得するのによく使用する[`Random`クラス](https://msdn.microsoft.com/ja-jp/library/system.random%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396)についての話です。
+
+## これまでのRandomクラス
+
+試しに以下のコードを.NET Frameworkや.NET Core 1.x系、Monoで実行すると(タイミングにもよりますが)全部同じ値が出力されると思います。
+
+```csharp
+for (var i = 0; i < 10; i ++)
+{
+ Console.WriteLine(new Random().Next());
+}
+```
+
+これは結構有名な話で`Random`クラスは引数なしのコンストラクタでインスタンス化すると、内部で使用する乱数を生成するためのシード値に[`Environment.TickCount`](https://msdn.microsoft.com/ja-jp/library/system.environment.tickcount%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396)(OSが起動してからの時間)を使用するので、ミリ秒未満でインスタンスが複数生成されると同一のシード値になって生成される乱数も同じになってしまうという現象です。
+
+シングルスレッドで動くアプリケーションなら同時に`Random`クラスがほぼ同時に複数インスタンス生成されることがないため問題はないかもしれませんが、Webアプリケーションのようにマルチスレッドで動くアプリケーションの場合は`Random`クラスを使っているロジックに同時にアクセスされてしまうとランダム性が保証されなくなってしまいます。
+
+Webアプリケーションを複数台で運用していて、サーバーのプロセスが同時に再起動が走ったときに`Environment.TickCount`がサーバー間で同じ値になってしまうため、各サーバーに同時にアクセスが来た時も同じ乱数を返してしまうためランダム性がなくなってしまいます。
+
+また、引数なしのコンストラクタを使わずにシード値を渡すコンストラクタで常に同じ値を渡すようにしても、インスタンスが生成される度に同じ乱数生成の元の値になってしまうため、これでもランダム性の問題は解決できません。
+(試しに以下のコードを実行してみるとループの数だけ同じ値が出力されます。)
+
+```csharp
+for (var i = 0; i < 10; i++)
+{
+ var random = new Random(0);
+
+ Console.WriteLine(random.Next());
+ Console.WriteLine(random.Next());
+}
+```
+
+これらのことから`Random`クラスを完全にランダム性を確保しつつ使用するには、インスタンスを生成する度にシード値に別の乱数を使用する必要があります。
+
+.NET Core 2.0ではこの問題が解決されているようです。
+
+## .NET Core 2.0でのRandom
+
+.NET Core 2.0では引数なしのコンストラクタの仕様が変わっていて、シード値に`Environment.TickCount`ではなく、内部で別に乱数を生成してその値をシード値にするようになっています。
+
+そのためさっきのコードを.NET Core 2.0で実行すると全て違う値が出力されるようになると思います。
+
+ちょっとDeep Diveしてみると以下のようなロジックでシード値を生成しています。
+
+```csharp
+[ThreadStatic]
+private static Random t_threadRandom;
+private static readonly Random s_globalRandom = new Random(GenerateGlobalSeed());
+
+private static int GenerateSeed()
+{
+ Random rnd = t_threadRandom;
+ if (rnd == null)
+ {
+ int seed;
+ lock (s_globalRandom)
+ {
+ seed = s_globalRandom.Next();
+ }
+ rnd = new Random(seed);
+ t_threadRandom = rnd;
+ }
+ return rnd.Next();
+}
+
+private static unsafe int GenerateGlobalSeed()
+{
+ int result;
+ Interop.GetRandomBytes((byte*)&result, sizeof(int));
+ return result;
+}
+```
+
+まずプロセスで唯一の`Random`インスタンスを乱数を用いたシード値で生成し、スレッドごとにプロセス唯一`Random`インスタンスから生成した乱数をシード値した`Random`インスタンスを生成し、そのスレッドごとに生成された`Random`インスタンスから生成した乱数をシード値をするようになっています。
+(ちなみにプロセス唯一の`Random`インスタンスを`lock`しながら乱数を生成しているのは`Next()`メソッドがスレッドセーフではないためです。)
+
+ちょっとわかりにくいですが、`Random`を生成するための`Random`を生成するための`Random`があるといった感じになっているようです。
+
+.NET Core 2.0以前では以下のような感じでシード値に使用する乱数を生成していました。([@neuecc](https://twitter.com/neuecc)さんの[ブログ](http://neue.cc/2013/03/06_399.html)より拝借。)
+
+```csharp
+public int GenerateSeed()
+{
+ using (var rng = new RNGCryptoServiceProvider())
+ {
+ var buffer = new byte[sizeof(int)];
+ rng.GetBytes(buffer);
+
+ return BitConverter.ToInt32(buffer, 0);
+ }
+}
+```
+
+ってことでどっちが速いの、というのを[BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet)を使って以下のロジックで計測してみました。
+
+```csharp
+public class RandomProvider
+{
+ [Benchmark]
+ public void Default()
+ {
+ var r = new Random();
+ }
+
+ [Benchmark]
+ public void Custom()
+ {
+ using (var rng = new RNGCryptoServiceProvider())
+ {
+ var buffer = new byte[sizeof(int)];
+ rng.GetBytes(buffer);
+ var seed = BitConverter.ToInt32(buffer, 0);
+
+ var r = new Random(seed);
+ }
+ }
+}
+```
+
+計測結果は以下の通り。
+
+![image.png](https://qiita-image-store.s3.amazonaws.com/0/39403/939f5683-c78a-1a51-da0a-4c82df9b103e.png)
+
+まぁ、誤差っちゃ誤差ですけど自前でシード値を生成するよりかはちょっとだけ速いようです。
+
+## まとめ
+
+プログラミングにおいてランダム性の確保って結構難しいって言われてるようですが、フレームワークとかライブラリに乱数生成の機能がある場合、それをそのまま使ってる方はいるんじゃないかと思います。
+
+ただその乱数生成が何時如何なるときも本当にランダム性が確保できているかの確認はした方が良さそうですね。
+
+ちなみに.NETの`Random`クラスの厄介なこととしてシード値以外にもう一つあって、本文中でもちょっと書いたのですが乱数を取得するメソッドがスレッドセーフじゃないんですね。
+
+この問題は.NET Core 2.0でも変わってないようなのでマルチスレッドで乱数を生成するロジックを書く場合は、`Random`クラスを`ThreadLocal`などでラップして使う必要はあるので注意です。
+
+ちなみに各フレームワークの`Random`クラスのソースは以下の通りです。
+(.NET Coreは2.0になってmscorlibの関係かcorefxからcoreclrに移ったのが結構あるようで。)
+* [.NET Framework](https://referencesource.microsoft.com/#mscorlib/system/random.cs)
+* [.NET Core 1.0](https://github.com/dotnet/corefx/blob/release/1.0.0/src/System.Runtime.Extensions/src/System/Random.cs)
+* [.NET Core 1.1](https://github.com/dotnet/corefx/blob/release/1.1.0/src/System.Runtime.Extensions/src/System/Random.cs)
+* [.NET Core 2.0](https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/Random.cs)
+* [Mono](https://github.com/mono/mono/blob/0bcbe39b148bb498742fc68416f8293ccd350fb6/mcs/class/referencesource/mscorlib/system/random.cs)