Ist der Random.Next()
Methoden-Thread von C # sicher?
Ist der Random.Next()
Methoden-Thread von C # sicher?
Antworten:
Bei der Next
Methode zur Gewährleistung der Gewindesicherheit wird nichts Besonderes getan . Es ist jedoch eine Instanzmethode. Wenn Sie Instanzen nicht Random
über verschiedene Threads hinweg gemeinsam nutzen, müssen Sie sich keine Gedanken über die Statusbeschädigung innerhalb einer Instanz machen. Verwenden Sie keine einzelne Instanz Random
über verschiedene Threads hinweg, ohne eine exklusive Sperre zu haben.
Jon Skeet hat ein paar nette Beiträge zu diesem Thema:
StaticRandom
Wiederholung der Zufälligkeit
Wie von einigen Kommentatoren festgestellt, gibt es ein weiteres potenzielles Problem bei der Verwendung verschiedener Instanzen Random
, die Thread-exklusiv sind, aber identisch ausgesät werden und daher die identischen Sequenzen von Pseudozufallszahlen induzieren, da sie zur gleichen Zeit oder innerhalb eines engen Zeitrahmens erstellt werden können Nähe voneinander. Eine Möglichkeit, dieses Problem zu beheben, besteht darin, eine Master- Random
Instanz (die von einem einzelnen Thread gesperrt wird) zu verwenden, um zufällige Startwerte zu generieren und neue Random
Instanzen für jeden anderen zu verwendenden Thread zu initialisieren .
Random
Wenn aufgrund der Art und Weise, wie erstellt wurde, zwei separate Instanzen von Random
auf zwei separaten Threads fast gleichzeitig erstellt werden, haben sie dieselben Startwerte (und geben somit dieselben Werte zurück). Siehe meine Antwort für eine Problemumgehung.
Random
erfordert das orthogonale Problem der statistischen Beziehung zwischen zwei verschiedenen Instanzen natürlich weitere Sorgfalt.
Nein, die Verwendung derselben Instanz aus mehreren Threads kann dazu führen, dass sie unterbrochen wird und alle Nullen zurückgibt. Das Erstellen einer thread-sicheren Version (ohne dass bei jedem Aufruf böse Sperren erforderlich sind Next()
) ist jedoch einfach. Angepasst an die Idee in diesem Artikel :
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();
}
}
Die Idee ist, static Random
für jeden Thread eine separate Variable zu behalten . Dies auf offensichtliche Weise zu tun, schlägt jedoch aufgrund eines anderen Problems fehl: Random
Wenn mehrere Instanzen fast gleichzeitig (innerhalb von ca. 15 ms) erstellt werden , geben alle dieselben Werte zurück! Um dies zu beheben, erstellen wir eine global statische Random
Instanz, um die von jedem Thread verwendeten Seeds zu generieren.
Der obige Artikel enthält übrigens Code, der diese beiden Probleme mit demonstriert Random
.
ThreadSafeRandom
zu nutzen. Warum nicht eine statische Eigenschaft mit einem Lazy Getter verwenden, der derzeit den Construtors-Code enthält? Idee von hier: konfluenz.jetbrains.com/display/ReSharper/… Dann kann die gesamte Klasse statisch sein.
IRandom
und eine Klasse, die Aufrufe zur Laufzeit an eine statische Datei umleitet. Dies würde das Verspotten ermöglichen. Nur für mich sind alle Mitglieder statisch, was bedeutet, dass ich eine statische Klasse haben sollte, es sagt dem Benutzer mehr, es sagt, dass jede Instanz keine separate Folge von Zufallszahlen ist, sie wird geteilt. Dies ist der Ansatz, den ich zuvor gewählt habe, wenn ich eine statische Framework-Klasse verspotten muss.
_local
kann im Konstruktor nicht instanziiert werden.
Die offizielle Antwort von Microsoft ist ein sehr starkes Nein . Von http://msdn.microsoft.com/en-us/library/system.random.aspx#8 :
Zufällige Objekte sind nicht threadsicher. Wenn Ihre App Zufallsmethoden aus mehreren Threads aufruft, müssen Sie ein Synchronisationsobjekt verwenden, um sicherzustellen, dass jeweils nur ein Thread auf den Zufallszahlengenerator zugreifen kann. Wenn Sie nicht sicherstellen, dass auf das Random-Objekt threadsicher zugegriffen wird, geben Aufrufe von Methoden, die Zufallszahlen zurückgeben, 0 zurück.
Wie in den Dokumenten beschrieben, gibt es einen sehr schlimmen Nebeneffekt, der auftreten kann, wenn dasselbe zufällige Objekt von mehreren Threads verwendet wird: Es funktioniert einfach nicht mehr.
(dh es gibt eine Race-Bedingung, bei deren Auslösung der Rückgabewert der Methoden 'random.Next ....' für alle nachfolgenden Aufrufe 0 ist.)
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.
Nein, es ist nicht threadsicher. Wenn Sie dieselbe Instanz aus verschiedenen Threads verwenden müssen, müssen Sie die Verwendung synchronisieren.
Ich kann jedoch keinen Grund erkennen, warum Sie das brauchen würden. Es wäre effizienter, wenn jeder Thread eine eigene Instanz der Random-Klasse hätte.
Eine andere thread-sichere Methode ist die Verwendung ThreadLocal<T>
wie folgt:
new ThreadLocal<Random>(() => new Random(GenerateSeed()));
Die GenerateSeed()
Methode muss bei jedem Aufruf einen eindeutigen Wert zurückgeben, um sicherzustellen, dass die Zufallszahlenfolgen in jedem Thread eindeutig sind.
static int SeedCount = 0;
static int GenerateSeed() {
return (int) ((DateTime.Now.Ticks << 4) +
(Interlocked.Increment(ref SeedCount)));
}
Funktioniert für eine kleine Anzahl von Threads.
++SeedCount
führt eine Rennbedingung ein. Verwenden Sie Interlocked.Increment
stattdessen.
Da dies Random
nicht threadsicher ist, sollten Sie eine pro Thread anstelle einer globalen Instanz haben. Wenn Sie befürchten, dass diese mehreren Random
Klassen gleichzeitig ausgesät werden (dh von DateTime.Now.Ticks
oder so), können Sie Guid
s verwenden, um jede von ihnen zu säen. Der .NET- Guid
Generator unternimmt erhebliche Anstrengungen, um nicht wiederholbare Ergebnisse sicherzustellen. Daher:
var rnd = new Random(BitConverter.ToInt32(Guid.NewGuid().ToByteArray(), 0))
NewGuid
sind praktisch eindeutig, die ersten 4 Bytes dieser GUIDs (das ist alles, was BitConverter.ToInt32
betrachtet wird) jedoch nicht. Grundsätzlich ist es eine schreckliche Idee , Teilzeichenfolgen von GUIDs als einzigartig zu behandeln .
Guid.NewGuid
, zumindest unter Windows, GUIDs der Version 4 verwenden , die meist zufällig generiert werden. Insbesondere werden die ersten 32 Bits zufällig generiert, sodass Sie Ihre Random
Instanz im Wesentlichen nur mit einer (vermutlich kryptografisch?) Zufallszahl mit einer Kollisionswahrscheinlichkeit von 1 zu 2 Milliarden versehen. Ich habe stundenlang nachgeforscht, um dies festzustellen, und ich habe immer noch keine Ahnung, wie sich .NET Core NewGuid()
unter Nicht-Windows-Betriebssystemen verhält.
Für was es wert ist, hier ist ein thread-sicheres, kryptografisch starkes RNG, das erbt Random
.
Die Implementierung enthält statische Einstiegspunkte, um die Verwendung zu vereinfachen. Sie haben dieselben Namen wie die öffentlichen Instanzmethoden, jedoch mit dem Präfix "Get".
Ein Anruf bei der RNGCryptoServiceProvider.GetBytes
ist eine relativ teure Operation. Dies wird durch die Verwendung eines internen Puffers oder "Pools" gemindert, um eine weniger häufige und effizientere Nutzung zu ermöglichen RNGCryptoServiceProvider
. Wenn eine Anwendungsdomäne nur wenige Generationen enthält, kann dies als Overhead angesehen werden.
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());
? Sollte das nicht einfach so sein SyncRoot = new object();
?
Pro Dokumentation
Alle öffentlichen statischen (in Visual Basic freigegebenen) Mitglieder dieses Typs sind threadsicher. Es wird nicht garantiert, dass Instanzmitglieder threadsicher sind.
Random
hat keine statischen Mitglieder, daher heißt es in den zitierten Dokumenten effektiv, dass nicht garantiert ist , dass alle Mitglieder threadsicher sind. . Sie geben an, dass dies falsch ist, und bestätigen dies, indem Sie sagen, dass ... Random
nicht threadsicher ist? Das ist so ziemlich das Gleiche, was die Ärzte gesagt haben!
Neuimplementierung der Antwort von BlueRaja mit 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);
}
}
Für einen thread sicheren Zufallszahlengenerator schauen Sie sich an RNGCryptoServiceProvider . Aus den Dokumenten:
Gewindesicherheit
Dieser Typ ist threadsicher.
AKTUALISIERT: Ist es nicht. Sie müssen entweder eine Instanz von Random bei jedem aufeinanderfolgenden Aufruf wiederverwenden, indem Sie beim Aufrufen der Methode .Next () ein "Semaphor" -Objekt sperren, oder bei jedem solchen Aufruf eine neue Instanz mit einem garantierten zufälligen Startwert verwenden. Sie können den garantierten unterschiedlichen Startwert erhalten, indem Sie die Kryptografie in .NET verwenden, wie von Yassir vorgeschlagen.
Der herkömmliche Ansatz zur lokalen Speicherung von Threads kann durch Verwendung eines Algorithmus ohne Sperre für den Startwert verbessert werden. Folgendes wurde schamlos aus Javas Algorithmus gestohlen (möglicherweise sogar verbessert ):
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
besiegt den Punkt davon. Java's Random
wird mit a gesetzt long
, aber C # nimmt nur ein int ... und noch schlimmer, es verwendet den absoluten Wert dieses signierten int als Seed, was bedeutet, dass es effektiv nur 2 ^ 31 verschiedene Seeds gibt. Eine gute long
Samenerzeugung ist eine Art Verschwendung, wenn Sie dann die meisten Teile davon wegwerfen long
. Zufälliges Seeding von C # ist zufällig, selbst wenn Ihr zufälliges Seed vollkommen zufällig ist, haben Sie dennoch eine Kollisionswahrscheinlichkeit von ungefähr 1 zu 2 Milliarden.