13

このテストを実行した場合:

 var r = new Random();
 var ints = new int[13];
 Parallel.For(0, 2000000, i => {            
     var result = r.Next(1, 7) + r.Next(1, 7);
     ints[result] += 1;
 });

次の結果が得られます。

2: 92,14445
3: 0,41765
4: 0,62245
5: 0,82525
6: 1,04035
7: 1,25215
8: 1,0531
9: 0,8341
10: 0,6334
11: 0,4192
12: 0,2109

通常のForを使用する場合:

for (int i = 0; i < 2000000; i++) {
    var result = r.Next(1, 7) + r.Next(1, 7);
    ints[result] += 1;
}

出力は次のとおりです。

2: 2,7797
3: 5,58645
4: 8,3414
5: 11,09935
6: 13,8909
7: 16,6731
8: 13,82895
9: 11,10205
10: 8,3424
11: 5,5712
12: 2,7845

最後の結果は三角分布であり、期待される出力です。

私の質問の目的は、並列処理の適用可能性について説明することではありません。問題は、Parallel.Forがそのように動作する理由です。

4

3 に答える 3

26

Randomクラスメソッドはスレッドセーフではありません。

http://msdn.microsoft.com/en-us/library/system.random.next(v=vs.90).aspx#2

したがって、最初のコードは、未定義の動作を示しているだけです。

編集:

少し推測すると、オペレーティングシステムについて私がほとんど知らないことから、乱数の生成はかなり低レベルの操作であり、したがってコンテキストスイッチが必要になることさえあると思います。これが発生している間、更新の機会が得られる前に、同じ乱数を複数回取得してしまう可能性があります。これは、偏った分布を説明します。

于 2012-11-13T12:07:07.600 に答える
9

Randomクラスはスレッドセーフではないという@spencerruportの主張に加えて、並列コードもスレッドセーフではありません。

 Parallel.For(0, 2000000, i => {            
     //say two threads produce same total at same time
     var result = r.Next(1, 7) + r.Next(1, 7); 
     //what happens on the next line when a context-switch
     //occurs during this non-atomic operation?
     ints[result] += 1;
 });

PLINQを活用して、ユーザーに代わって結果を収集する方がよい場合があります。

Enumerable.Range(0, 2000000)
    .AsParallel()
    .Select(_ => SafeRandom(1, 7) + SafeRandom(1, 7))
    .GroupBy(x => x)
    .Select(g => new {value = g.Key, frequency = g.Count()})

共有メモリ(ints上記のアレイ)へのアクセスを自分で管理する代わりに。

の合理的な実装は次のSafeRandomようになります。

private static int seedUnique=0;
private static ThreadLocal<Random> tlRand=new ThreadLocal<Random>(() => {
    var x=Interlocked.Add(ref seedUnique, 93459872);
    var r=new Random((int)(DateTime.UtcNow.Ticks + x));
    return r;
});
public static int SafeRandom(int min, int max)
{
    return tlRand.Value.Next(min,max);
}
于 2012-11-13T12:16:52.260 に答える
3

のスレッドセーフですRandom

Random.Next()スレッドセーフを呼び出すと、期待どおりに以下のディストリビューションが得られます。

2: 2.76665
3: 5.5382
4: 8.30805
5: 11.13095
6: 13.8864
7: 16.6808
8: 13.8722
9: 11.14495
10: 8.3409
11: 5.5631
12: 2.76775

public static class Program
{
    private const int Max = 2000000;
    private static readonly object Lock = new object();

    public static void Main()
    {
        var r = new Random();
        var ints = new int[13];
        Parallel.For(0, Max, i =>
        {
            var result = Rand(r, 1, 7) + Rand(r, 1, 7);
            Interlocked.Increment(ref ints[result]);
        });

        for (int i = 0; i < ints.Length; i++)
        {
            Console.WriteLine("{0}: {1}",
                i, ints[i] / ((double)Max) * 100);
        }
    }

    private static int Rand(Random random, int minValue, int maxValue)
    {
        lock (Lock)
        {
            return random.Next(minValue, maxValue);
        }
    }
}
于 2012-11-13T12:52:24.383 に答える