13

If I run this test:

 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;
 });

I get the following result:

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

When I use the regular For:

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

The output is:

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

The last result is a Triangular Distribution and it is the expected output.

The purpose of my question is not to discuss the applicability of parallelism. The question is why the Parallel.For behaves that way?

Douglas H. M.
  • 442
  • 4
  • 11

3 Answers3

26

The Random class methods are not thread safe.

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

So the first piece of code is just demonstrating some undefined behavior.

EDIT:

As for a little speculation, from what little I know about operating systems I believe random number generation is a pretty low level operation and hence might even require a context switch. While this is happening you may end up grabbing the same random number multiple times before it's had a chance to update. This would account for the lopsided distribution.

Spencer Ruport
  • 34,865
  • 12
  • 85
  • 147
9

In addition to @spencerruport's assertion that the Random class is not thread safe, your parallel code is also not threadsafe:

 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;
 });

It might be better to leverage PLINQ to do the collecting of results on your behalf:

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

instead of managing access to shared memory (your ints array above) yourself.

A reasonable implementation of SafeRandom might look something like this:

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);
}
spender
  • 117,338
  • 33
  • 229
  • 351
  • Yep. look into using thread-local variables. Some of the other overloads of Parallel.For would work just fine for this application. See http://msdn.microsoft.com/en-us/library/dd460703.aspx for an example. – Fabian Tamp Nov 13 '12 at 12:18
3

It is thread safety of Random.

I get the below distribution as expected once I've made the call to Random.Next() thread safe.

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);
        }
    }
}
M Afifi
  • 4,645
  • 2
  • 28
  • 48
  • 1
    Yes, but still *not* threadsafe in the body of `Parallel.For`. – spender Nov 13 '12 at 12:54
  • Parallel access to the shared `ints` array. You're going to have many threads operating on the same indexes of the array. See my answer. Do your counts add up to 2000000? I'll bet not, because of this problem (although the issue *may* be mitigated by passing through the lock on the previous method call, I wouldn't rely on this). – spender Nov 13 '12 at 12:57
  • @spender I'd like to see some x64 code after the JIT has run or an authoritative source from Microsoft before I believe claims that ++ is not thread safe. Unless Microsoft has gone out of its way to make it not thread safe, ++ is a single atomic operation. – M Afifi Nov 13 '12 at 13:05
  • 1
    Load, yes. Add, yes. Store, yes. Load, Add and store? Sorry, you're wrong. Why do you think the Interlocked class exists? http://msdn.microsoft.com/en-us/library/dd78zt0c.aspx – spender Nov 13 '12 at 13:06
  • Looks like Eric Lippert addressed this before here, http://stackoverflow.com/questions/4628243/is-the-operator-thread-safe . Hard to understand how the JIT team decided to take a single CPU instruction and turn it into several. – M Afifi Nov 13 '12 at 13:23