.NET Tip #38: Thread-safe Random

Tomáš Jecha, MVP, MCSD       25.01.2012       C#, .NET Tips       12713 zobrazení

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();
    }
}

 

hodnocení článku

1 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Diskuse: .NET Tip #38: Thread-safe Random

Dobrý den, mám možná hloupou otázku, proč jsou ta třída a její metody statické?

Děkuji

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Tento kód řeší sám problém vytváření a přístupu k instancím, který je popsaný v textu. A proto jsou metody statické. Je jedno kdy a z jakého vlákna jej zavoláte.

Jinými slovy, metody jsou statické schválně, protože kód této třídy slouží právě k tomu, aby jste se instancemi nemusel zabýval.

nahlásit spamnahlásit spam 0 / 2 odpovědětodpovědět

Děkuji :-)

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Diskuse: .NET Tip #38: Thread-safe Random

Pak je jen otázka nakolik onen lin. kongruentní generátor vygeneruje rozumná čísla :) (odpověď: jsou velmi predikovatelná) Nicméně v situaci, kdy chci jen vygenerovat "nějaké číslo" to asi stačí.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Otázka je spíš - kdy potřebuješ "rozumnější" náhodná čísla? :-)

Krom nějakých kryptografických operací, kde na něco takového je generování vestavěné většinou uvnitř (nebo lze využít jiný generátor převážně ze sekce kryptografických providerů), tak to není potřeba. Nebo já jsem se s takovým případem nesetkal.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback