Параллельные. Для и для получения разных результатов
Если я запустил этот тест:
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 (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
Последний результат - треугольное распределение, и это ожидаемый результат.
Цель моего вопроса - не обсуждать применимость parallelism. Вопрос в том, почему Parallel.For ведет себя таким образом?
Ответы
Ответ 1
Методы класса Random не являются потокобезопасными.
http://msdn.microsoft.com/en-us/library/system.random.next(v=vs.90).aspx#2
Итак, первая часть кода просто демонстрирует поведение undefined.
EDIT:
Что касается небольшой спекуляции, из того, что мало что известно о операционных системах, я считаю, что генерация случайных чисел - операция довольно низкого уровня и, следовательно, может потребоваться даже контекстный переключатель. В то время как это происходит, вы можете в конечном итоге схватить одно и то же случайное число несколько раз, прежде чем у него появилась возможность обновиться. Это будет учитывать однобокое распределение.
Ответ 2
В дополнение к утверждению @spencerruport, что класс Random не является потокобезопасным, ваш параллельный код также не является потокобезопасным:
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);
}
Ответ 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);
}
}
}