4

Noticed that the crypto-random number generator isn't thread safe when generating random numbers in parallel over multiple threads. The generator used is RNGCryptoServiceProvider and it appears to repeat long stretches of random bits (128 bits). The code to reproduce this is shown below.

Short of using a lock to guard access to the RNGCryptoServiceProvider instance (which kills the whole speed point here), does anyone have a faster way to generator crypto-random numbers?

using System;
using System.Runtime.Caching;
using System.Security.Cryptography;
using System.Threading.Tasks;

namespace ParallelRandomness
{
    class Program
    {
        static void Main(string[] args)
        {
            var test = new Test();
            Console.Write("Serialized verion running ...  ");
            test.Run(false);
            Console.WriteLine();
            Console.Write("Parallelized verion running ...  ");
            test.Run(true);
            Console.WriteLine(Environment.NewLine + "Done.");
            Console.ReadLine();
        }
    }

    class Test
    {
        private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
        private readonly byte[] _randomBytes = new byte[128 / 8];
        private int collisionCount = 0;
        private readonly object collisionCountLock  = new object();

        public void Run(bool parallel)
        {
            const int numOfRuns = 100000;
            const int startInclusive = 1;
            const int endExclusive = numOfRuns + startInclusive;
            if (parallel)
                Parallel.For(startInclusive, endExclusive, x => GenRandomByteArrays(x));
            else
            {
                for (var i = startInclusive; i < endExclusive; i++)
                    GenRandomByteArrays(i);
            }
        }

        private void GenRandomByteArrays(long instance)
        {
            _rng.GetBytes(_randomBytes);
            var randomString = Convert.ToBase64String(_randomBytes);
            var cache = MemoryCache.Default;
            if (cache.Contains(randomString))
            {
                // uh-oh!
                lock (collisionCountLock)
                {
                    Console.WriteLine(Environment.NewLine + "Instance {0}: Collision count={1}. key={2} already in cache. ", instance, ++collisionCount, randomString);
                }
            }
            else
            {
                MemoryCache.Default.Add(randomString, true, DateTimeOffset.UtcNow.AddMinutes(5));
                Console.Write(instance % 2 == 0 ? "\b-" : "\b|"); // poor man's activity indicator
            }
        }
    }
}
DeepSpace101
  • 13,110
  • 9
  • 77
  • 127
  • possible duplicate of [Is C# Random Number Generator thread safe?](http://stackoverflow.com/questions/3049467/is-c-sharp-random-number-generator-thread-safe) – Oblivious Sage Jun 04 '14 at 19:50
  • (Some of) the answers to that question provide fast *and* thread-safe methods of generating random numbers. – Oblivious Sage Jun 04 '14 at 19:54
  • The for `RNGCryptoServiceProvider` documentation explicitly states: "This type is thread safe." – CodesInChaos Jun 04 '14 at 19:56
  • @ObliviousSage: Actually that class in question (`Random`) is not even crypto-random, so it's thread-safety and performance is irrelevant. – DeepSpace101 Jun 04 '14 at 19:56
  • 1
    Aren't you using the same array in multiple threads? That's obviously not safe even if the PRNG itself is safe. – CodesInChaos Jun 04 '14 at 19:58
  • @CodesInChaos: Doh! Yeah, that's the problem! You want to put that as the answer so I can give credit? – DeepSpace101 Jun 04 '14 at 20:01

1 Answers1

10

The documentation for RNGCryptoServiceProvider states:

This type is thread safe.

Your code doesn't demonstrate that RNGCryptoServiceProvider is not thread-safe, since you use the same array in multiple threads. That array reuse is not thread-safe even if RNGCryptoServiceProvider is.

Regarding performance I want to note that creating a new instance of RNGCryptoServiceProvider is very cheap. The expensive part is the per-call overhead of GetBytes.

So if you have performance trouble, the first thing I'd try is asking for more data in one go and splitting it yourself. If that's still not enough, use a stream cipher seeded by the system CSPRNG.

Eric Mutta
  • 1,144
  • 1
  • 11
  • 15
CodesInChaos
  • 106,488
  • 23
  • 218
  • 262
  • 1
    By all means use a stream cypher to stretch initial entropy, but you **must** seed it with a crypto CSRNG, not the system PRNG which is not secure. Reseed regularly from the CSPRNG so you do not get toolong a run from the stream cypher. Crypto is as strong as its weakest link, and a non-crypto PRNG is weak. – rossum Jun 05 '14 at 21:56
  • 1
    @rossum By the system PRNG I mean `/dev/urandom` or `CryptGenRandom`. Not the crappy `System.Random` included with .net. – CodesInChaos Jun 06 '14 at 08:17