Fast thread-safe random number generator for C#

2019-03-06 21:45发布

问题:

I need to quickly generate random floating-point numbers across multiple running threads. I've tried using System.Random, but it's too slow for my needs and it returns the same number across multiple threads. (It works fine when I run my application in a single thread.) Also, I need to make sure the generated numbers are between 0 and 100.

Here's what I'm trying now:

number = random.NextDouble() * 100;

What should I try instead?

回答1:

Here is my take on it (requires .net 4.0):

public static class RandomGenerator
{
    private static object locker = new object();
    private static Random seedGenerator = new Random(Environment.TickCount);

    public static double GetRandomNumber()
    {
        int seed;

        lock (locker)
        {
            seed = seedGenerator.Next(int.MinValue, int.MaxValue);
        }

        var random = new Random(seed);

        return random.NextDouble();
    }
}

and a test to check that for 1000 iterations each value is unique:

[TestFixture]
public class RandomGeneratorTests
{
    [Test]
    public void GetRandomNumber()
    {
        var collection = new BlockingCollection<double>();

        Parallel.ForEach(Enumerable.Range(0, 1000), i =>
        {
            var random = RandomGenerator.GetRandomNumber();
            collection.Add(random);
        });

        CollectionAssert.AllItemsAreUnique(collection);
    }
}

I don't guarantee that it will never return a duplicate value, but I've run the test with 10000 iterations and it passed the test.



回答2:

I use the windows cryptoAPI for good random numbers. For performance I do a single call for a block of 8KB of random data and distribute numbers from that instead of call the cryptoAPI for each number. Not sure what the performance is in the end compared to the normal random. But the randomization is far better (check the internet for details on the Windows CryptoAPI)

This is the code;

// UNIT RandomNumberGeneratorBase
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FastLibrary
{
    public abstract class RandomNumberGeneratorBase
{    
        private int _byteBufSize;
        private byte[] _buf;
        private int _idx;
        private int _lastsize;

        public RandomNumberGeneratorBase(int bufSize = 8092)
    {    
            _byteBufSize = bufSize;
            _buf = new byte[_byteBufSize];
            _idx = _byteBufSize;
        }

        protected abstract void GetNewBuf(byte[] buf);

        private void CheckBuf(int bytesFreeNeeded = 1)
        {    
            _idx += _lastsize;
            _lastsize = bytesFreeNeeded;
            if (_idx + bytesFreeNeeded < _byteBufSize) { return; }
            GetNewBuf(_buf);
            _idx      = 0;
            _lastsize = 0;
        }

        public byte GetRandomByteStartAtZero(int belowValue)
       {    
         return (byte)(Math.Round(((double)GetRandomByte() * (belowValue - 1)) / 255));
       }    

        public int GetRandomIntStartAtZero(int belowValue)
       {    
            return (int)(Math.Round(((double)GetRandomUInt32() * (double)(belowValue - 1)) / (double)uint.MaxValue));
       }    

        public byte GetRandomByte()
    {    
            CheckBuf();
        return _buf[_idx];
    }    

        public bool GetRandomBool()
    {    
            CheckBuf();
        return _buf[_idx] > 127;
    }    

        public ulong GetRandomULong()
    {    
            CheckBuf(sizeof(ulong));
        return BitConverter.ToUInt64(_buf, _idx);
    }    

        public int GetRandomInt()
    {    
            CheckBuf(sizeof(int));
        return BitConverter.ToInt32(_buf, _idx);
    }    

        /// <summary>
        ///     Double from 0 to 1 (might be zero, will never be 1)
        /// </summary>
        public double GetRandomDouble()
    {    
            return GetRandomUInt32() / (1d + UInt32.MaxValue);
    }    

        /// <summary>
        ///     Float from 0 to 1 (might be zero, will never be 1)
        /// </summary>
        public float GetRandomFloat()
    {    
            return GetRandomUInt32() / (1f + UInt32.MaxValue);
    }    

        public uint GetRandomUInt32()
    {    
            CheckBuf(sizeof(UInt32));
            return BitConverter.ToUInt32(_buf, _idx);
    }    
    }    
}    

// UNIT StrongRandomNumberGenerator
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace FastLibrary
{
    public sealed class StrongRandomNumberGenerator : RandomNumberGeneratorBase
{    
        private RNGCryptoServiceProvider _rnd;

        public StrongRandomNumberGenerator()
    {    
            _rnd = new RNGCryptoServiceProvider();
    }    

        protected override void GetNewBuf(byte[] buf)
    {    
            _rnd.GetBytes(buf);
    }    

    }
}    


回答3:

If Random is giving you the same numbers then you're probably using it incorrectly, either by creating many instances in close succession (meaning that they'll all use the same seed and so generate the same sequence), or by using a single instance across several threads (thereby "breaking" that instance since it's not safe for multithreaded use).

If the speed and randomness of Random are good enough for you when running in a single thread then you could try wrapping it in a ThreadLocal<T> to ensure a separate instance for each thread in your multithreaded scenario:

var number = _rng.Value.NextDouble() * 100;

// ...

private static int _staticSeed = Environment.TickCount;
private static readonly ThreadLocal<Random> _rng = new ThreadLocal<Random>(() =>
    {
        int seed = Interlocked.Increment(ref _staticSeed) & 0x7FFFFFFF;
        return new Random(seed);
    });