From 2c82c70f970471c402e1d87f164ea58af4972dea Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 23 Sep 2020 21:10:55 +0200 Subject: [PATCH] First implementation --- FastRng/MultiThreadedRng.cs | 113 ++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 FastRng/MultiThreadedRng.cs diff --git a/FastRng/MultiThreadedRng.cs b/FastRng/MultiThreadedRng.cs new file mode 100644 index 0000000..ca588e8 --- /dev/null +++ b/FastRng/MultiThreadedRng.cs @@ -0,0 +1,113 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace FastRng +{ + public sealed class MultiThreadedRng : IRandom + { + #if DEBUG + private const int CAPACITY_RANDOM_NUMBERS_4_SOURCE = 10_000; + #else + private const int CAPACITY_RANDOM_NUMBERS_4_SOURCE = 16_000_000; + #endif + + private readonly System.Random rng = new System.Random(); + private readonly CancellationTokenSource producerToken = new CancellationTokenSource(); + + private readonly Thread producerRandom1; + private readonly Thread producerRandom2; + + private readonly Channel channelRandom = Channel.CreateBounded(new BoundedChannelOptions(CAPACITY_RANDOM_NUMBERS_4_SOURCE) + { + FullMode = BoundedChannelFullMode.Wait, + SingleReader = false, + SingleWriter = false, + }); + + #region Constructors + + public MultiThreadedRng() + { + this.producerRandom1 = new Thread(() => MultiThreadedRng.RandomProducer(this.rng, this.channelRandom.Writer, this.producerToken.Token)) {IsBackground = true}; + this.producerRandom2 = new Thread(() => MultiThreadedRng.RandomProducer(this.rng, this.channelRandom.Writer, this.producerToken.Token)) {IsBackground = true}; + this.producerRandom1.Start(); + this.producerRandom2.Start(); + } + + public MultiThreadedRng(int seed) + { + this.rng = new Random(seed); + + this.producerRandom1 = new Thread(() => MultiThreadedRng.RandomProducer(this.rng, this.channelRandom.Writer, this.producerToken.Token)) {IsBackground = true}; + this.producerRandom2 = new Thread(() => MultiThreadedRng.RandomProducer(this.rng, this.channelRandom.Writer, this.producerToken.Token)) {IsBackground = true}; + + this.producerRandom1.Start(); + this.producerRandom2.Start(); + } + + #endregion + + [ExcludeFromCodeCoverage] + private static async void RandomProducer(System.Random random, ChannelWriter channelWriter, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + // + // We using double as basis for anything. That's what .NET does internally as well, cf. https://github.com/dotnet/runtime/blob/6072e4d3a7a2a1493f514cdf4be75a3d56580e84/src/libraries/System.Private.CoreLib/src/System/Random.cs. + // random.NextDouble() returns Sample(). Next(min, max) uses GetSampleForLargeRange(). + // Thus, we re-implement GetSampleForLargeRange() and use its numbers as source for everything. + // + + var buffer = new double[CAPACITY_RANDOM_NUMBERS_4_SOURCE]; + for (var n = 0; n < buffer.Length && !cancellationToken.IsCancellationRequested; n++) + { + #region Re-implementation of GetSampleForLargeRange() method of .NET + + var result = random.Next(); // Notice: random.Next() is identical to InternalSample() + var negative = random.Next() % 2 == 0; // Notice: random.Next() is identical to InternalSample() + if (negative) + result = -result; + + double d = result; + d += (int.MaxValue - 1); // get a number in range [0 .. 2 * Int32MaxValue - 1) + d /= 2 * (uint)int.MaxValue - 1; + + #endregion + + buffer[n] = d; + } + + for (var n = 0; n < buffer.Length && !cancellationToken.IsCancellationRequested; n++) + await channelWriter.WriteAsync(buffer[n], cancellationToken); + } + } + + #region Implementing interface + + public async Task NextNumber(uint rangeStart, uint rangeEnd, CancellationToken cancel = default(CancellationToken)) + { + var range = rangeEnd - rangeStart; + return (uint) ((await this.channelRandom.Reader.ReadAsync(cancel) * range) + rangeStart); + } + + public async Task NextNumber(ulong rangeStart, ulong rangeEnd, CancellationToken cancel = default(CancellationToken)) + { + var range = rangeEnd - rangeStart; + return (ulong) ((await this.channelRandom.Reader.ReadAsync(cancel) * range) + rangeStart); + } + + public async Task NextNumber(float rangeStart, float rangeEnd, CancellationToken cancel = default(CancellationToken)) + { + var range = rangeEnd - rangeStart; + return (float) ((await this.channelRandom.Reader.ReadAsync(cancel) * range) + rangeStart); + } + + public void StopProducer() => this.producerToken.Cancel(); + + #endregion + } +} \ No newline at end of file