Třída Random slouží ke generování pseudonáhodných čísel. Pokud ji ale chcete používat v konkurenčním prostředí, můžete narazit na několik zásadních problémů.
Sdílená instance mezi vlákny
Sdílet volně instanci třídy Random mezi více vlákny je velmi špatný nápad. Její metody totiž nejsou thread-safe a může se vám povést vyvolat vícekrát interní volání a sadu vzorků pro generování náhodných čísel znehodnotit a generátor začne vracet po nějaké době jako výsledek pouze jedno číslo.
Zde je jediná možnost obalit přístup k této instanci příkazem lock. To je nejjednodušší, z pohledu výkonu to může ale způsobovat problémy, pokud chcete náhodných čísel generovat hodně.
Vytváření nových instancí pro každé číslo
Druhou možností je inicializovat novou instanci při každém požadavku na náhodné číslo. Toto řešení selže při vytváření instancí ihned po sobě. Jako vstupní číslo definující pseudonáhodnou řadu se totiž používá aktuální čas (Environment.TickCount) a ten můžeme stihnout získat vícekrát stejný a tím pádem budou náhodná čísla ve stejný čas stejná. Například na mém počítači se v nekonečné smyčce stihne vygenerovat přibližně 5000 stejných čísel po sobě. Jako spolehlivě řešení to tedy také nelze využít.
Správné řešení – kombinovaný generátor
Jako nejsprávnější a zároveň nejrychlejší řešení vidím v možnosti při které vytváříme pro každé vlákno vlastní instanci třídy Random a nemůže tak dojít ke kolizi z více vláken. Toho dosáhneme atributem [ThreadStatic] určující, že proměnná má unikátní hodnotu pro každé vlákno. Zároveň díky Garbage Collectoru bude takto definovaná proměnná automaticky uvolněna po ukončení vlákna.
Dále je potřeba eliminovat šanci vytvoření instance třídy Random se stejnou inicializační hodnotou vyvoláním příkazů ve stejnou chvíli. K tomu používám sdílenou instanci (v kódu seedGenerator) jen pro generování právě oné inicializační hodnoty (seed) – nevyužívám tedy aktuální čas. Volání tohoto sdíleného generátoru musím ošetřovat zámkem (seedGeneratorLock). Jeho volání se však děje jen jednou za život vlákna a proto se jedná o zanedbatelné zpomalení.
Do kódu jsem implementoval všechny metody, které můžete volat na třídě Random a proto je její využívání jednodušší.
public static class RandomThreadSafe
{
static Random seedGenerator;
static object seedGeneratorLock;
[ThreadStatic]
static Random threadStaticRandom;
static RandomThreadSafe()
{
seedGenerator = new Random();
seedGeneratorLock = new object();
}
private static Random GetRandomInstance()
{
if (threadStaticRandom == null)
{
// random object for current thread is not created yet
int seed;
// generate random seed
lock (seedGeneratorLock)
{
// seed should be only non-negative numbers
seed = seedGenerator.Next(0, int.MaxValue);
}
// create instance
threadStaticRandom = new Random(seed);
}
return threadStaticRandom;
}
/// <summary>
/// Returns a nonnegative random number.
/// </summary>
/// <returns>A 32-bit signed integer greater than or equal to zero and less than System.Int32.MaxValue.</returns>
public static int Next()
{
return GetRandomInstance().Next();
}
/// <summary>
/// Returns a nonnegative random number less than the specified maximum.
/// </summary>
/// <param name="maxValue">The exclusive upper bound of the random number to be generated.
/// maxValue must be greater than or equal to zero.</param>
/// <returns>A 32-bit signed integer greater than or equal to zero, and less than maxValue;
/// that is, the range of return values ordinarily includes zero but not maxValue.
/// However, if maxValue equals zero, maxValue is returned.</returns>
/// <exception cref="System.ArgumentOutOfRangeException">maxValue is less than zero.</exception>
public static int Next(int maxValue)
{
return GetRandomInstance().Next(maxValue);
}
/// <summary>
/// Returns a random number within a specified range.
/// </summary>
/// <param name="minValue">The inclusive lower bound of the random number returned.</param>
/// <param name="maxValue">The exclusive upper bound of the random number returned. maxValue must be
/// greater than or equal to minValue.</param>
/// <returns>A 32-bit signed integer greater than or equal to minValue and less than maxValue;
/// that is, the range of return values includes minValue but not maxValue. If
/// minValue equals maxValue, minValue is returned.</returns>
/// <exception cref="System.ArgumentOutOfRangeException">minValue is greater than maxValue.</exception>
public static int Next(int minValue, int maxValue)
{
return GetRandomInstance().Next(minValue, maxValue);
}
/// <summary>
/// Fills the elements of a specified array of bytes with random numbers.
/// </summary>
/// <param name="buffer">An array of bytes to contain random numbers.</param>
/// <exception cref="System.ArgumentNullException">buffer is null.</exception>
public static void NextBytes(byte[] buffer)
{
GetRandomInstance().NextBytes(buffer);
}
/// <summary>
/// Returns a random number between 0.0 and 1.0.
/// </summary>
/// <returns>A double-precision floating point number greater than or equal to 0.0, and less than 1.0.</returns>
public static double NextDouble()
{
return GetRandomInstance().NextDouble();
}
}