Skip to content

Commit 653818f

Browse files
authored
Fix for non-uniform random value generators (StefH#26)
* Added failing unit test * Fix identified. System.Random requires a lock for multithread access * Issue resolved. Random values are now uniform * Moved tests to a dedicated folder
1 parent 642978a commit 653818f

File tree

5 files changed

+184
-25
lines changed

5 files changed

+184
-25
lines changed

src/RandomDataGenerator Solution.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RandomDataGenerator.Gui3",
3939
EndProject
4040
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "ConsoleApp\ConsoleApp.csproj", "{13262322-06A6-4CB7-AABC-78DA60730E07}"
4141
EndProject
42+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{51B9C7D7-C306-485A-8EA9-38B32E928E95}"
43+
EndProject
44+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RandomDataGenerator.Tests", "..\tests\RandomDataGenerator.Tests\RandomDataGenerator.Tests.csproj", "{ED709720-2669-444E-AC90-ABB0B76A894A}"
45+
EndProject
4246
Global
4347
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4448
Debug|Any CPU = Debug|Any CPU
@@ -77,6 +81,10 @@ Global
7781
{13262322-06A6-4CB7-AABC-78DA60730E07}.Debug|Any CPU.Build.0 = Debug|Any CPU
7882
{13262322-06A6-4CB7-AABC-78DA60730E07}.Release|Any CPU.ActiveCfg = Release|Any CPU
7983
{13262322-06A6-4CB7-AABC-78DA60730E07}.Release|Any CPU.Build.0 = Release|Any CPU
84+
{ED709720-2669-444E-AC90-ABB0B76A894A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
85+
{ED709720-2669-444E-AC90-ABB0B76A894A}.Debug|Any CPU.Build.0 = Debug|Any CPU
86+
{ED709720-2669-444E-AC90-ABB0B76A894A}.Release|Any CPU.ActiveCfg = Release|Any CPU
87+
{ED709720-2669-444E-AC90-ABB0B76A894A}.Release|Any CPU.Build.0 = Release|Any CPU
8088
EndGlobalSection
8189
GlobalSection(SolutionProperties) = preSolution
8290
HideSolutionNode = FALSE
@@ -90,6 +98,7 @@ Global
9098
{94DA986E-B36A-4A9A-8B1E-F5BBAE82BEE7} = {CD43A6FA-4DEF-47B3-A430-9E7FFAD6B035}
9199
{89571CAF-8CA9-44C2-98AB-D476CA2458DF} = {CD43A6FA-4DEF-47B3-A430-9E7FFAD6B035}
92100
{13262322-06A6-4CB7-AABC-78DA60730E07} = {F8306255-6F4A-4E70-9932-06B2A3C9DF78}
101+
{ED709720-2669-444E-AC90-ABB0B76A894A} = {51B9C7D7-C306-485A-8EA9-38B32E928E95}
93102
EndGlobalSection
94103
GlobalSection(ExtensibilityGlobals) = postSolution
95104
SolutionGuid = {F00DE8F4-BF0D-49C3-8854-600E8142BE41}

src/RandomDataGenerator/Generators/RandomValueGenerator.cs

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ namespace RandomDataGenerator.Generators
1313
internal class RandomValueGenerator
1414
{
1515
private const double Tolerance = double.Epsilon;
16-
private Random _rnf = new();
16+
static Random _rnf = new();
17+
static readonly object RandomLock = new object();
1718
private double _storedUniformDeviate;
1819
private bool _storedUniformDeviateIsGood;
1920

@@ -27,7 +28,10 @@ public RandomValueGenerator(int seed)
2728

2829
public void Reset(int seed)
2930
{
30-
_rnf = new Random(seed);
31+
lock (RandomLock)
32+
{
33+
_rnf = new Random(seed);
34+
}
3135
}
3236
#endregion
3337

@@ -38,23 +42,32 @@ public void Reset(int seed)
3842
/// </summary>
3943
public double Next()
4044
{
41-
return _rnf.NextDouble();
45+
lock (RandomLock)
46+
{
47+
return _rnf.NextDouble();
48+
}
4249
}
4350

4451
/// <summary>
4552
/// Returns true or false randomly.
4653
/// </summary>
4754
public bool NextBoolean()
4855
{
49-
return _rnf.Next(0, 2) != 0;
56+
lock (RandomLock)
57+
{
58+
return _rnf.Next(0, 2) != 0;
59+
}
5060
}
5161

5262
/// <summary>
5363
/// Returns double in the range [0, 1)
5464
/// </summary>
5565
public double NextDouble()
5666
{
57-
return _rnf.NextDouble();
67+
lock (RandomLock)
68+
{
69+
return _rnf.NextDouble();
70+
}
5871
}
5972

6073
/// <summary>
@@ -68,7 +81,10 @@ public byte[] NextBytes(int min, int max)
6881
int arrayLength = Next(min, max);
6982

7083
byte[] bytes = new byte[arrayLength];
71-
_rnf.NextBytes(bytes);
84+
lock (RandomLock)
85+
{
86+
_rnf.NextBytes(bytes);
87+
}
7288

7389
return bytes;
7490
}
@@ -140,8 +156,11 @@ public byte Next(byte min, byte max)
140156
throw new ArgumentException("Max must be greater than min.");
141157
}
142158

143-
double rn = (max * 1.0 - min * 1.0) * _rnf.NextDouble() + min * 1.0;
144-
return Convert.ToByte(rn);
159+
lock (RandomLock)
160+
{
161+
double rn = (max * 1.0 - min * 1.0) * _rnf.NextDouble() + min * 1.0;
162+
return Convert.ToByte(rn);
163+
}
145164
}
146165

147166
/// <summary>
@@ -154,16 +173,22 @@ public short Next(short min, short max)
154173
throw new ArgumentException("Max must be greater than min.");
155174
}
156175

157-
double rn = (max * 1.0 - min * 1.0) * _rnf.NextDouble() + min * 1.0;
158-
return Convert.ToInt16(rn);
176+
lock (RandomLock)
177+
{
178+
double rn = (max * 1.0 - min * 1.0) * _rnf.NextDouble() + min * 1.0;
179+
return Convert.ToInt16(rn);
180+
}
159181
}
160182

161183
/// <summary>
162184
/// Returns Int32 in the range [min, max)
163185
/// </summary>
164186
public int Next(int min, int max)
165187
{
166-
return _rnf.Next(min, max);
188+
lock (RandomLock)
189+
{
190+
return _rnf.Next(min, max);
191+
}
167192
}
168193

169194
/// <summary>
@@ -176,8 +201,11 @@ public long Next(long min, long max)
176201
throw new ArgumentException("Max must be greater than min.");
177202
}
178203

179-
double rn = (max * 1.0 - min * 1.0) * _rnf.NextDouble() + min * 1.0;
180-
return Convert.ToInt64(rn);
204+
lock (RandomLock)
205+
{
206+
double rn = (max * 1.0 - min * 1.0) * _rnf.NextDouble() + min * 1.0;
207+
return Convert.ToInt64(rn);
208+
}
181209
}
182210

183211
/// <summary>
@@ -190,8 +218,11 @@ public float Next(float min, float max)
190218
throw new ArgumentException("Max must be greater than min.");
191219
}
192220

193-
double rn = (max * 1.0 - min * 1.0) * _rnf.NextDouble() + min * 1.0;
194-
return Convert.ToSingle(rn);
221+
lock (RandomLock)
222+
{
223+
double rn = (max * 1.0 - min * 1.0) * _rnf.NextDouble() + min * 1.0;
224+
return Convert.ToSingle(rn);
225+
}
195226
}
196227

197228
/// <summary>
@@ -204,8 +235,11 @@ public double Next(double min, double max)
204235
throw new ArgumentException("Max must be greater than min.");
205236
}
206237

207-
double rn = (max - min) * _rnf.NextDouble() + min;
208-
return rn;
238+
lock (RandomLock)
239+
{
240+
double rn = (max - min) * _rnf.NextDouble() + min;
241+
return rn;
242+
}
209243
}
210244

211245
/// <summary>
@@ -220,10 +254,13 @@ public DateTime Next(DateTime min, DateTime max)
220254

221255
long minTicks = min.Ticks;
222256
long maxTicks = max.Ticks;
223-
double rn = (Convert.ToDouble(maxTicks)
224-
- Convert.ToDouble(minTicks)) * _rnf.NextDouble()
225-
+ Convert.ToDouble(minTicks);
226-
return new DateTime(Convert.ToInt64(rn));
257+
lock (RandomLock)
258+
{
259+
double rn = (Convert.ToDouble(maxTicks)
260+
- Convert.ToDouble(minTicks)) * _rnf.NextDouble()
261+
+ Convert.ToDouble(minTicks);
262+
return new DateTime(Convert.ToInt64(rn));
263+
}
227264
}
228265

229266
/// <summary>
@@ -238,10 +275,13 @@ public TimeSpan Next(TimeSpan min, TimeSpan max)
238275

239276
long minTicks = min.Ticks;
240277
long maxTicks = max.Ticks;
241-
double rn = (Convert.ToDouble(maxTicks)
242-
- Convert.ToDouble(minTicks)) * _rnf.NextDouble()
243-
+ Convert.ToDouble(minTicks);
244-
return new TimeSpan(Convert.ToInt64(rn));
278+
lock (RandomLock)
279+
{
280+
double rn = (Convert.ToDouble(maxTicks)
281+
- Convert.ToDouble(minTicks)) * _rnf.NextDouble()
282+
+ Convert.ToDouble(minTicks);
283+
return new TimeSpan(Convert.ToInt64(rn));
284+
}
245285
}
246286

247287
/// <summary>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System.Collections.Concurrent;
2+
using RandomDataGenerator.FieldOptions;
3+
using RandomDataGenerator.Randomizers;
4+
using Xunit.Abstractions;
5+
6+
namespace RandomDataGenerator.Tests
7+
{
8+
public class CityRandomizerTests
9+
{
10+
private readonly ITestOutputHelper _output;
11+
static readonly Random random = new System.Random(420);
12+
static readonly object randLock = new object();
13+
14+
public CityRandomizerTests(ITestOutputHelper output)
15+
{
16+
this._output = output;
17+
}
18+
19+
20+
[Theory]
21+
[InlineData(1)]
22+
[InlineData(2)]
23+
[InlineData(4)]
24+
[InlineData(8)]
25+
public void CityDistributionMustBeUniform(int degree)
26+
{
27+
var locationGenerator = RandomizerFactory.GetRandomizer(new FieldOptionsCity { ValueAsString = true, UseNullValues = false });
28+
var concurrentDictionary = new ConcurrentDictionary<string, long>();
29+
var options = new ParallelOptions { MaxDegreeOfParallelism = degree };
30+
Parallel.For(0, 1000, options, i =>
31+
{
32+
Parallel.For(0, 1000, options, j =>
33+
{
34+
var location = locationGenerator.Generate();
35+
concurrentDictionary.AddOrUpdate(location, _ => 1, (k, v) => v + 1);
36+
});
37+
});
38+
var topCount = concurrentDictionary.OrderByDescending(pair => pair.Value).First();
39+
var bottomCount = concurrentDictionary.OrderBy(pair => pair.Value).First();
40+
_output.WriteLine($"{topCount}");
41+
_output.WriteLine($"{bottomCount}");
42+
Assert.True(topCount.Value/bottomCount.Value<2);
43+
Assert.NotEqual(topCount.Key, bottomCount.Key);
44+
}
45+
46+
[Fact]
47+
public void TwoRandomCitiesMustNotBeTheSame()
48+
{
49+
var locationGenerator = RandomizerFactory.GetRandomizer(new FieldOptionsCity { ValueAsString = true, UseNullValues = false });
50+
var locationOne = locationGenerator.Generate();
51+
var locationTwo = locationGenerator.Generate();
52+
Assert.NotEqual(locationOne, locationTwo);
53+
}
54+
55+
[Fact]
56+
public void SystemRandomDistributionMustBeUniform()
57+
{
58+
var concurrentDictionary = new ConcurrentDictionary<int, long>();
59+
Parallel.For(0, 1000, i =>
60+
{
61+
Parallel.For(0, 1000, j =>
62+
{
63+
int index;
64+
lock (randLock)
65+
{
66+
index = random.Next(0, 2000);
67+
}
68+
69+
concurrentDictionary.AddOrUpdate(index, _ => 1, (k, v) => v + 1);
70+
});
71+
});
72+
var topCount = concurrentDictionary.OrderByDescending(pair => pair.Value).First();
73+
var bottomCount = concurrentDictionary.OrderBy(pair => pair.Value).First();
74+
_output.WriteLine($"{topCount}");
75+
_output.WriteLine($"{bottomCount}");
76+
Assert.True(topCount.Value / bottomCount.Value < 2);
77+
Assert.NotEqual(topCount.Key, bottomCount.Key);
78+
79+
}
80+
}
81+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net7.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
13+
<PackageReference Include="xunit" Version="2.4.2" />
14+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
15+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
16+
<PrivateAssets>all</PrivateAssets>
17+
</PackageReference>
18+
<PackageReference Include="coverlet.collector" Version="3.1.2">
19+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20+
<PrivateAssets>all</PrivateAssets>
21+
</PackageReference>
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<ProjectReference Include="..\..\src\RandomDataGenerator\RandomDataGenerator.csproj" />
26+
</ItemGroup>
27+
28+
</Project>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
global using Xunit;

0 commit comments

Comments
 (0)