Чи Random.Next()
безпечний потік методу C # ?
Чи Random.Next()
безпечний потік методу C # ?
Відповіді:
У Next
методі нічого особливого не зроблено для досягнення безпеки ниток. Однак це метод екземпляра. Якщо ви не ділитеся екземплярами Random
між різними потоками, вам не доведеться турбуватися про корупцію в межах екземпляра. Не використовуйте жодного екземпляра Random
між різними потоками, не тримаючи ексклюзивного замка.
Джон Скіт має кілька приємних дописів на цю тему:
StaticRandom
Перегляд випадковості
Як зазначають деякі коментатори, існує ще одна потенційна проблема у використанні різних екземплярів, Random
які не виключають потоки, але породжуються однаково і, отже, індукують однакові послідовності псевдовипадкових чисел, оскільки вони можуть створюватися одночасно або в тісному часі близькість один одного. Одним із способів усунути цю проблему є використання головного Random
екземпляра (який заблокований одним потоком) для генерації випадкових насінин та ініціалізації нових Random
екземплярів для кожного іншого потоку, що використовується.
Random
було створено спосіб , якщо два окремі екземпляри Random
буде створено в двох окремих потоках майже одночасно, вони матимуть однакові насіння (і, отже, повертати ті самі значення). Дивіться мою відповідь на вирішення проблеми.
Random
випадках вимагає подальшої уваги.
Ні, використання одного і того ж екземпляра з декількох потоків може призвести до його злому та повернення всіх 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
.
ThreadSafeRandom
щоб використовувати це. Чому б не використовувати статичну властивість з лінивим геттером, який наразі містить код конструторів. Ідея звідси: confluence.jetbrains.com/display/ReSharper/ ... Тоді весь клас може бути статичним.
IRandom
, і клас, який перенаправляє виклики до статичного під час виконання, це дозволить глузувати. Лише для мене це всі члени статичні, це означає, що я повинен мати статичний клас, він повідомляє користувачеві більше, він говорить, що кожен екземпляр не є окремою послідовністю випадкових чисел, він є спільним. Це підхід, який я застосовував раніше, коли мені потрібно знущатися із статичного фреймворк-класу.
_local
неможливо створити екземпляр у конструкторі.
Офіційна відповідь від Microsoft - дуже рішуче " ні" . З http://msdn.microsoft.com/en-us/library/system.random.aspx#8 :
Випадкові об'єкти не захищені від потоків. Якщо ваша програма викликає випадкові методи з декількох потоків, ви повинні використовувати об'єкт синхронізації, щоб гарантувати, що лише один потік може одночасно отримувати доступ до генератора випадкових чисел. Якщо ви не гарантуєте доступ до випадкового об'єкта безпечним для потоку способом, виклики методів, що повертають випадкові числа, повертають 0.
Як описано в документації, є дуже неприємний побічний ефект, який може статися, коли один і той же Випадковий об'єкт використовується кількома потоками: він просто перестає працювати.
(тобто існує умова гонки, яка при спрацьовуванні повертається значення методів 'random.Next ....' буде 0 для всіх наступних викликів.)
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.
Ні, це не безпечно для ниток. Якщо вам потрібно використовувати один і той же екземпляр із різних потоків, вам доведеться синхронізувати використання.
Насправді я не бачу жодної причини, чому вам це потрібно. Для кожного потоку було б ефективніше мати власний екземпляр класу Random.
Іншим потокобезпечним способом є використання 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)));
}
Працюватиме для невеликої кількості ниток.
++SeedCount
вводить перегонові умови. Використовуйте Interlocked.Increment
замість цього.
Оскільки Random
не є безпечним для потоку, ви повинні мати по одному для кожного потоку, а не глобальний примірник. Якщо ви стурбовані тим, що ці кілька Random
класів висіваються одночасно (тобто тим DateTime.Now.Ticks
чи іншим), ви можете використовувати Guid
s для засівання кожного з них. Guid
Генератор .NET докладає значних зусиль, щоб забезпечити неповторні результати, отже:
var rnd = new Random(BitConverter.ToInt32(Guid.NewGuid().ToByteArray(), 0))
NewGuid
, практично гарантовано будуть унікальними, але перші 4 байти цих GUID (що все, що BitConverter.ToInt32
розглядається) - ні. Як загальний принцип, розглядати підрядки GUID як унікальні - це жахлива ідея .
Guid.NewGuid
, принаймні в Windows, використовує GUID версії 4 , які в основному генеруються випадковим чином. Зокрема, перші 32 біти генеруються випадковим чином, тому ви по суті просто засіваєте свій Random
екземпляр випадковим числом (імовірно, криптографічно?), З шансом зіткнення 1 на 2 мільярди. Мені знадобилися години досліджень, щоб визначити це, однак, і я досі не уявляю, як .NET Core NewGuid()
поводиться в ОС, які не є Windows.
Для чого він вартий, ось безпечний, криптографічно потужний 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();
?
Реалізація відповіді 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);
}
}
Lazy<Random> GlobalRandom
, щоб уникнути явного lock
. The
Random
одночасно 2 потоки доступу до глобальної ...
Lazy<T>
цьому випадку клас нічого не пропонує.
За документацією
Усі загальнодоступні статичні (спільно використовуються у Visual Basic) члени цього типу є потокобезпечними. Будь-які члени екземпляра не гарантують безпеку потоку.
Random
не має статичних членів, тому цитовані документи фактично заявляють, що всі його члени "не гарантовано захищені від потоків" . Ви заявляєте, що це неправильно, а потім підтверджуєте це тим, що ... Random
не є безпечним для потоку? Це майже те саме, що сказали документи!
Для потоково безпечного генератора випадкових чисел зверніться до RNGCryptoServiceProvider . З документів:
Безпека ниток
Цей тип захищений від ниток.
ОНОВЛЕНО: Це не так. Вам потрібно повторно використовувати екземпляр Random для кожного послідовного виклику, заблокувавши якийсь об'єкт "семафор" під час виклику методу .Next (), або використовувати новий екземпляр із гарантованим випадковим насінням для кожного такого виклику. Ви можете отримати гарантоване різне насіння, використовуючи криптографію в .NET, як запропонував Ясір.
Традиційний підхід локального зберігання потоків можна вдосконалити, використовуючи алгоритм без замикання для насіння. Наступне було безсоромно викрадено з алгоритму 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;
}
}
int
. Java Random
засіяна символом a long
, але C # просто приймає int ... і, що ще гірше, він використовує абсолютне значення цього підписаного int як насіння, тобто фактично лише 2 ^ 31 різних насінин. Наявність хорошого long
насіння - це свого роду марнотратство, якщо тоді ви викидаєте більшість шматочків long
; випадкове висівання випадкового C #, навіть якщо ваше випадкове насіння абсолютно випадкове, все одно залишає вам приблизно 1 з 2 мільярдів шансів зіткнення.