Чи безпечний потік генератора випадкових чисел C #?


79

Чи Random.Next()безпечний потік методу C # ?


1
“Будь-які загальнодоступні статичні (спільно використовуються у Visual Basic) члени цього типу є потокобезпечними. Будь-які члени екземпляра не гарантують безпеку потоку. документів на System.Random ). [Добре, чесно кажучи: там також пояснюється найпоширеніша проблема з псевдовипадковими числами, яку люди, здається, мають, і вони все ще продовжують запитувати]
Джої,

Відповіді:


33

У Nextметоді нічого особливого не зроблено для досягнення безпеки ниток. Однак це метод екземпляра. Якщо ви не ділитеся екземплярами Randomміж різними потоками, вам не доведеться турбуватися про корупцію в межах екземпляра. Не використовуйте жодного екземпляра Randomміж різними потоками, не тримаючи ексклюзивного замка.

Джон Скіт має кілька приємних дописів на цю тему:

StaticRandom
Перегляд випадковості

Як зазначають деякі коментатори, існує ще одна потенційна проблема у використанні різних екземплярів, Randomякі не виключають потоки, але породжуються однаково і, отже, індукують однакові послідовності псевдовипадкових чисел, оскільки вони можуть створюватися одночасно або в тісному часі близькість один одного. Одним із способів усунути цю проблему є використання головного Randomекземпляра (який заблокований одним потоком) для генерації випадкових насінин та ініціалізації нових Randomекземплярів для кожного іншого потоку, що використовується.


13
"Якщо ви не ділитесь екземплярами Random між різними потоками, вам не надто слід турбуватися." - Це хибно. Через те, як Randomбуло створено спосіб , якщо два окремі екземпляри Randomбуде створено в двох окремих потоках майже одночасно, вони матимуть однакові насіння (і, отже, повертати ті самі значення). Дивіться мою відповідь на вирішення проблеми.
BlueRaja - Danny Pflughoeft

6
@BlueRaja Я спеціально націлювався на державне питання корупції в межах однієї інстанції. Звичайно, як ви вже згадували, ортогональна проблема статистичних відносин у двох окремих Randomвипадках вимагає подальшої уваги.
mmx

23
Я не уявляю, чому це позначено як відповідь! З: "Чи безпечний Random.Next?" В: "Якщо ви використовуєте його лише з однієї нитки, то так, це безпечно для потоку" .... Найгірша відповідь!
Мік

Найгірше, що ви можете зробити, це подобатись зовнішнім статтям давати відповіді, а НЕ насправді включати відповідь ... Особливо, коли це так просто, як це.
Ентоні Ніколс

"вони матимуть однакові насіння" Це було виправлено у .Net Core.
Магнус

87

Ні, використання одного і того ж екземпляра з декількох потоків може призвести до його злому та повернення всіх 0. Однак створити безпечну для потоку версію (не потребуючи неприємних блокувань під час кожного дзвінка Next()) просто. Адаптовано з ідеї в цій статті :

public class ThreadSafeRandom
{
    private static readonly Random _global = new Random();
    [ThreadStatic] private static Random _local;

    public int Next()
    {
        if (_local == null)
        {
            lock (_global)
            {
                if (_local == null)
                {
                    int seed = _global.Next();
                    _local = new Random(seed);
                }
            }
        }

        return _local.Next();
    }
}

Ідея полягає в тому, щоб зберегти окрему static Randomзмінну для кожного потоку. Однак зробити це очевидним чином не вдається через іншу проблему Random- якщо кілька екземплярів створюються майже одночасно (протягом приблизно 15 мс) , вони повернуть однакові значення! Щоб це виправити, ми створюємо глобально-статичний Randomекземпляр для генерації насіння, що використовується кожним потоком.

У вищезазначеній статті, до речі, є код, що демонструє обидві ці проблеми з Random.


12
Приємно, але не подобається, як вам потрібно створити a, ThreadSafeRandomщоб використовувати це. Чому б не використовувати статичну властивість з лінивим геттером, який наразі містить код конструторів. Ідея звідси: confluence.jetbrains.com/display/ReSharper/ ... Тоді весь клас може бути статичним.
weston

2
@weston: Це зазвичай вважається анти-шаблоном. Окрім втрати всіх переваг ООП, основна причина полягає в тому, що статичні об'єкти не можна знущатись , що є життєво важливим для модульного тестування будь-яких класів, які використовують це.
BlueRaja - Danny Pflughoeft

3
Ви завжди можете додати шар опосередкованості, коли це потрібно, наприклад, додати an IRandom, і клас, який перенаправляє виклики до статичного під час виконання, це дозволить глузувати. Лише для мене це всі члени статичні, це означає, що я повинен мати статичний клас, він повідомляє користувачеві більше, він говорить, що кожен екземпляр не є окремою послідовністю випадкових чисел, він є спільним. Це підхід, який я застосовував раніше, коли мені потрібно знущатися із статичного фреймворк-класу.
weston

6
Код не буде працювати, якщо ви насправді використовуєте його з декількох потоків, оскільки _local не створюється щоразу, коли до нього здійснюється доступ: NullRefereceException.
Laie

3
Як вже згадувалося в коментарі раніше, цей підхід насправді не працює, коли використовується з декількох потоків. _localнеможливо створити екземпляр у конструкторі.
Олексій

23

Офіційна відповідь від Microsoft - дуже рішуче " ні" . З http://msdn.microsoft.com/en-us/library/system.random.aspx#8 :

Випадкові об'єкти не захищені від потоків. Якщо ваша програма викликає випадкові методи з декількох потоків, ви повинні використовувати об'єкт синхронізації, щоб гарантувати, що лише один потік може одночасно отримувати доступ до генератора випадкових чисел. Якщо ви не гарантуєте доступ до випадкового об'єкта безпечним для потоку способом, виклики методів, що повертають випадкові числа, повертають 0.

Як описано в документації, є дуже неприємний побічний ефект, який може статися, коли один і той же Випадковий об'єкт використовується кількома потоками: він просто перестає працювати.

(тобто існує умова гонки, яка при спрацьовуванні повертається значення методів 'random.Next ....' буде 0 для всіх наступних викликів.)


1
Існує більш неприємний побічний ефект від використання різних випадків випадкового об'єкта. він повертає одне і те ж сформоване число для декількох потоків. з тієї ж статті:Instead of instantiating individual Random objects, we recommend that you create a single Random instance to generate all the random numbers needed by your app. However, Random objects are not thread safe.
AaA


14

Ні, це не безпечно для ниток. Якщо вам потрібно використовувати один і той же екземпляр із різних потоків, вам доведеться синхронізувати використання.

Насправді я не бачу жодної причини, чому вам це потрібно. Для кожного потоку було б ефективніше мати власний екземпляр класу Random.


Це може вас вкусити за дупу, якщо у вас є об’єкт одиничного тестування і ви хочете генерувати тонни тестових об’єктів одночасно. Причиною цього є те, що багато людей роблять Random глобальним об’єктом для зручності використання. Я щойно зробив це, і rand.Next () продовжував генерувати 0 як значення aa.
JSWork,

1
@JSWork: Я насправді не слідую, що ти маєш на увазі. На що ви посилаєтесь, коли говорите "це"? Якщо я правильно розумію ваше останнє речення, ви отримали доступ до об’єкта через нитки, не синхронізуючи його, що пояснювало б результат.
Guffa,

1
Ви праві. Перепрошую - я це погано сформулював. Якщо не зробити те, про що ви згадали, це може вас вкусити за дупу. Як застереження, читачі також повинні бути обережними, створюючи новий випадковий об'єкт для кожної нитки - випадкові об'єкти використовують поточний час як своє затравочне джерело. Між змінами насіння існує розрив у 10 мс.
JSWork,

3
@JSWork: Так, виникає проблема із синхронізацією, якщо потоки запускаються одночасно. Ви можете використовувати один Випадковий об'єкт у головному потоці, щоб надати насіння для створення Випадкових об'єктів, щоб потоки могли обійти це.
Guffa

1
Якщо ви хочете зробити окремий випадковий для кожного потоку, використовуйте щось більш унікальне для кожного потоку для насіння, наприклад, ідентифікатор потоку або час + ідентифікатор потоку або подібне.
apokryfos

9

Іншим потокобезпечним способом є використання ThreadLocal<T>наступного:

new ThreadLocal<Random>(() => new Random(GenerateSeed()));

GenerateSeed()Метод потрібно буде повертати унікальне значення кожен раз , коли він викликається , щоб гарантувати , що послідовності випадкових чисел є унікальними в кожному потоці.

static int SeedCount = 0;
static int GenerateSeed() { 
    return (int) ((DateTime.Now.Ticks << 4) + 
                   (Interlocked.Increment(ref SeedCount))); 
}

Працюватиме для невеликої кількості ниток.


1
Але в цьому випадку, чи не переконується він у тому, що метод не повинен бути потокобезпечним .... кожен потік матиме доступ до своєї власної копії об’єкта.
проміжок

4
++SeedCountвводить перегонові умови. Використовуйте Interlocked.Incrementзамість цього.
Едвард Брей,

1
Як зазначає OP, це буде працювати з обмеженою кількістю потоків, це, мабуть, буде поганим вибором всередині ASP.NET
Кріс Марісіч

1
Думаю, ви можете замінити виклик GenerateSeed () у конструкторі Random на: Guid.NewGuid (). GetHashCode ())
Сіаваш Мортазаві

5

Оскільки Randomне є безпечним для потоку, ви повинні мати по одному для кожного потоку, а не глобальний примірник. Якщо ви стурбовані тим, що ці кілька Randomкласів висіваються одночасно (тобто тим DateTime.Now.Ticksчи іншим), ви можете використовувати Guids для засівання кожного з них. GuidГенератор .NET докладає значних зусиль, щоб забезпечити неповторні результати, отже:

var rnd = new Random(BitConverter.ToInt32(Guid.NewGuid().ToByteArray(), 0))

2
-1; GUID, сформовані за допомогою NewGuid, практично гарантовано будуть унікальними, але перші 4 байти цих GUID (що все, що BitConverter.ToInt32розглядається) - ні. Як загальний принцип, розглядати підрядки GUID як унікальні - це жахлива ідея .
Mark Amery

2
Єдине, що використовує цей підхід у цьому конкретному випадку, це те, що .NET Guid.NewGuid, принаймні в Windows, використовує GUID версії 4 , які в основному генеруються випадковим чином. Зокрема, перші 32 біти генеруються випадковим чином, тому ви по суті просто засіваєте свій Randomекземпляр випадковим числом (імовірно, криптографічно?), З шансом зіткнення 1 на 2 мільярди. Мені знадобилися години досліджень, щоб визначити це, однак, і я досі не уявляю, як .NET Core NewGuid()поводиться в ОС, які не є Windows.
Mark Amery

@MarkAmery В ОП не вказано, чи потрібна криптографічна якість, тому я вважаю, що відповідь все ще корисна як однокласник для швидкого кодування в некритичних ситуаціях. На основі вашого першого коментаря я змінив код, щоб уникнути перших чотирьох байтів.
Гленн Слейден,

1
Використання других 4 байт замість перших 4 не допомагає; коли я сказав, що перші 4 байти GUID не гарантовано будуть унікальними, я мав на увазі те, що жодні 4 байти GUID не повинні бути унікальними; є весь 16-байтовий GUID, але не будь-яка його менша частина. Ви насправді погіршили ситуацію зі своїми змінами, оскільки для GUID версії 4 (використовується .NET у Windows) другі 4 байти включають 4 невипадкові біти з фіксованими значеннями; ваше редагування зменшило кількість можливих значень насіння до низьких сотень мільйонів.
Mark Amery

Добре, дякую. Я скасував зміни, і люди можуть прислухатися до ваших коментарів, якщо у них є стурбованість, яку ви висловили.
Glenn Slayden

4

Для чого він вартий, ось безпечний, криптографічно потужний RNG, який успадковує Random.

Реалізація включає статичні точки входу для зручності використання, вони мають ті самі назви, що і загальнодоступні методи екземпляра, але мають префікс "Отримати".

Дзвінок на RNGCryptoServiceProvider.GetBytesце відносно дорога операція. Це пом'якшується завдяки використанню внутрішнього буфера або "Пулу" для менш частого та більш ефективного використання RNGCryptoServiceProvider. Якщо в домені програми мало поколінь, це можна розглядати як накладні витрати.

using System;
using System.Security.Cryptography;

public class SafeRandom : Random
{
    private const int PoolSize = 2048;

    private static readonly Lazy<RandomNumberGenerator> Rng =
        new Lazy<RandomNumberGenerator>(() => new RNGCryptoServiceProvider());

    private static readonly Lazy<object> PositionLock =
        new Lazy<object>(() => new object());

    private static readonly Lazy<byte[]> Pool =
        new Lazy<byte[]>(() => GeneratePool(new byte[PoolSize]));

    private static int bufferPosition;

    public static int GetNext()
    {
        while (true)
        {
            var result = (int)(GetRandomUInt32() & int.MaxValue);

            if (result != int.MaxValue)
            {
                return result;
            }
        }
    }

    public static int GetNext(int maxValue)
    {
        if (maxValue < 1)
        {
            throw new ArgumentException(
                "Must be greater than zero.",
                "maxValue");
        }
        return GetNext(0, maxValue);
    }

    public static int GetNext(int minValue, int maxValue)
    {
        const long Max = 1 + (long)uint.MaxValue;

        if (minValue >= maxValue)
        {
            throw new ArgumentException(
                "minValue is greater than or equal to maxValue");
        }

        long diff = maxValue - minValue;
        var limit = Max - (Max % diff);

        while (true)
        {
            var rand = GetRandomUInt32();
            if (rand < limit)
            {
                return (int)(minValue + (rand % diff));
            }
        }
    }

    public static void GetNextBytes(byte[] buffer)
    {
        if (buffer == null)
        {
            throw new ArgumentNullException("buffer");
        }

        if (buffer.Length < PoolSize)
        {
            lock (PositionLock.Value)
            {
                if ((PoolSize - bufferPosition) < buffer.Length)
                {
                    GeneratePool(Pool.Value);
                }

                Buffer.BlockCopy(
                    Pool.Value,
                    bufferPosition,
                    buffer,
                    0,
                    buffer.Length);
                bufferPosition += buffer.Length;
            }
        }
        else
        {
            Rng.Value.GetBytes(buffer);
        }
    }

    public static double GetNextDouble()
    {
        return GetRandomUInt32() / (1.0 + uint.MaxValue);
    }

    public override int Next()
    {
        return GetNext();
    }

    public override int Next(int maxValue)
    {
        return GetNext(0, maxValue);
    }

    public override int Next(int minValue, int maxValue)
    {
        return GetNext(minValue, maxValue);
    }

    public override void NextBytes(byte[] buffer)
    {
        GetNextBytes(buffer);
    }

    public override double NextDouble()
    {
        return GetNextDouble();
    }

    private static byte[] GeneratePool(byte[] buffer)
    {
        bufferPosition = 0;
        Rng.Value.GetBytes(buffer);
        return buffer;
    }

    private static uint GetRandomUInt32()
    {
        uint result;
        lock (PositionLock.Value)
        {
            if ((PoolSize - bufferPosition) < sizeof(uint))
            {
                GeneratePool(Pool.Value)
            }

            result = BitConverter.ToUInt32(
                Pool.Value,
                bufferPosition);
            bufferPosition+= sizeof(uint);
        }

        return result;
    }
}

Яка мета PositionLock = new Lazy<object>(() => new object());? Це не повинно бути просто SyncRoot = new object();?
Chris Marisic

@ChrisMarisic, я дотримувався зразка, наведеного нижче. Однак мінімальна вигода, якщо така існує, для лінивого створення блокування, тому ваша пропозиція здається обґрунтованою. csharpindepth.com/articles/general/singleton.aspx#lazy
Джодрелл

Це виглядає чудовим рішенням, але у мене є декілька запитань, чому використовувати BitConverter.ToUInt32 проти BitConverter.ToInt32, щоб він очистив GetNext ()? А навіщо басейн робити статичним? Це може врятувати вас від декількох пулів, але в одночасних системах, де у вас багато екземплярів SafeRandom, це також може стати горлом. Як засіяти RNGCryptoServiceProvider?
Wouter

3

Реалізація відповіді BlueRaja за допомогою ThreadLocal:

public static class ThreadSafeRandom
{
    private static readonly System.Random GlobalRandom = new Random();
    private static readonly ThreadLocal<Random> LocalRandom = new ThreadLocal<Random>(() => 
    {
        lock (GlobalRandom)
        {
            return new Random(GlobalRandom.Next());
        }
    });

    public static int Next(int min = 0, int max = Int32.MaxValue)
    {
        return LocalRandom.Value.Next(min, max);
    }
}

Для додаткових пунктів стилю ви могли б використовувати a Lazy<Random> GlobalRandom, щоб уникнути явного lock. The
Теодор Зуліас,

1
@TheodorZoulias не впевнений, що я дотримуюся, проблема полягає не просто в ініціалізації - якщо ми скинемо глобальний замок, ми можемо мати Randomодночасно 2 потоки доступу до глобальної ...
Охад Шнайдер

1
О, ти маєш рацію. Я не знаю, про що я думав. У Lazy<T>цьому випадку клас нічого не пропонує.
Теодор Зуліас,

2

За документацією

Усі загальнодоступні статичні (спільно використовуються у Visual Basic) члени цього типу є потокобезпечними. Будь-які члени екземпляра не гарантують безпеку потоку.

http://msdn.microsoft.com/en-us/library/system.random.aspx


2
Документація, яку ви цитували, насправді неправильна. Я вважаю, що це машинно створений вміст. Вміст спільноти до цієї теми на MSDN містить багато інформації, чому тип Random не є безпечним для потоків і як вирішити цю проблему (або за допомогою криптографії, або за допомогою "семафорів")

2
@MTG Ваш коментар розгублений. Randomне має статичних членів, тому цитовані документи фактично заявляють, що всі його члени "не гарантовано захищені від потоків" . Ви заявляєте, що це неправильно, а потім підтверджуєте це тим, що ... Randomне є безпечним для потоку? Це майже те саме, що сказали документи!
Mark Amery

Чубчик головою на столі. Так, ви праві: "Це майже те саме, що сказали документи!"
Сіетл Леонард,

1

Для потоково безпечного генератора випадкових чисел зверніться до RNGCryptoServiceProvider . З документів:

Безпека ниток

Цей тип захищений від ниток.


1
Так, RNGCryptoServiceProvider - це золото. Набагато краще, ніж Random, хоча є трохи більше роботи, щоб змусити його виплюнути певний тип числа (оскільки він генерує випадкові байти).
Рангоріч,

1
@Rangoric: Ні, це не краще ніж Random для будь-яких цілей, де можна використовувати будь-яку з них. Якщо вам потрібна випадковість для цілей шифрування, Випадковий клас не є можливим, для будь-якої цілі, де ви можете вибрати, Випадковий клас швидший і простіший у використанні.
Guffa,

Простота використання @Guffa - це одноразова річ, хоча, як я вже запхав її в бібліотеку, це насправді не є гарним моментом. Бути швидшим - це вірна точка, хоча я б волів, щоб справжня випадковість була на відміну від виглядає непогано. І для цього я також хочу, щоб це було захищено від ниток. Хоча зараз я маю намір перевірити це, щоб побачити, наскільки він повільніший (Random виробляє дублі і перетворює їх на те, що ви просите, тому це може навіть залежати від того, який саме діапазон чисел вам потрібен)
Rangoric

У C # я виявляю, що з дуже базовою реалізацією RNGCrypto це приблизно 100: 1 залежно від точної кількості, яку шукають (127 робить удвічі краще, ніж 128, наприклад). Далі я планую додати різьбу і подивитися, як це виходить (чому ні :))
Рангорік,

Оновіть до вищезазначеного твердження. Я довів його до 2: 1 для діапазонів менше 256 значень, і робить це краще, чим ближче до коефіцієнта 256 те число, яке ми хочемо.
Рангоріч,

-1

ОНОВЛЕНО: Це не так. Вам потрібно повторно використовувати екземпляр Random для кожного послідовного виклику, заблокувавши якийсь об'єкт "семафор" під час виклику методу .Next (), або використовувати новий екземпляр із гарантованим випадковим насінням для кожного такого виклику. Ви можете отримати гарантоване різне насіння, використовуючи криптографію в .NET, як запропонував Ясір.


-1

Традиційний підхід локального зберігання потоків можна вдосконалити, використовуючи алгоритм без замикання для насіння. Наступне було безсоромно викрадено з алгоритму Java (можливо, навіть вдосконалено ):

public static class RandomGen2 
{
    private static readonly ThreadLocal<Random> _rng = 
                       new ThreadLocal<Random>(() => new Random(GetUniqueSeed()));

    public static int Next() 
    { 
        return _rng.Value.Next(); 
    } 

    private const long SeedFactor = 1181783497276652981L;
    private static long _seed = 8682522807148012L;

    public static int GetUniqueSeed()
    {
        long next, current;
        do
        {
            current = Interlocked.Read(ref _seed);
            next = current * SeedFactor;
        } while (Interlocked.CompareExchange(ref _seed, next, current) != current);
        return (int)next ^ Environment.TickCount;
   } 
}

1
Суть цього неминучий (неминучий) актор int. Java Randomзасіяна символом a long, але C # просто приймає int ... і, що ще гірше, він використовує абсолютне значення цього підписаного int як насіння, тобто фактично лише 2 ^ 31 різних насінин. Наявність хорошого longнасіння - це свого роду марнотратство, якщо тоді ви викидаєте більшість шматочків long; випадкове висівання випадкового C #, навіть якщо ваше випадкове насіння абсолютно випадкове, все одно залишає вам приблизно 1 з 2 мільярдів шансів зіткнення.
Марк Амері,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.