Чи 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чи іншим), ви можете використовувати Guids для засівання кожного з них. 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 мільярдів шансів зіткнення.