Laden...
FAQ

[FAQ] Random.Next liefert eine Zeit lang die gleiche Zufallszahl - Warum? Wie geht es richtig?

Erstellt von xxMUROxx vor 12 Jahren Letzter Beitrag vor 12 Jahren 25.056 Views
xxMUROxx Themenstarter:in
1.552 Beiträge seit 2010
vor 12 Jahren
[FAQ] Random.Next liefert eine Zeit lang die gleiche Zufallszahl - Warum? Wie geht es richtig?

Hallo Community,

Das Generieren einer zufälligen Zahlenfolge wird oft benötigt; dazu bietet sich die Random-Klasse an. Jedoch wird leider mit dieser Klasse sehr oft falsch umgegangen.

Folgende Fragen gibt es in diesem Forum zur Genüge:*Warum werden immer dieselben zufälligen Zahlen generiert? *Random funktioniert nicht wie erwartet.

Die Klasse selbst ist korrekt, die Ursache für die Probleme liegt in der Benutzung.

1. Gleicher Seed - Gleiche Zufallsreihenfolge

Nehmen wir mal die erste Überladung der Konstruktors unter die Lupe: diese nimmt einen Seed, welcher für die Berechnung der Zufallszahlen essentiell ist, als Parameter.

Den Seed an sich kann man als eine Art Startwert für die Generierung der Zufallszahlen sehen. Die Tatsache ist so, dass es keinen perfekten Zufallszahlenalgorithmus gibt und somit die anfänglich übermittelte Zahl zur Berechnung der Zufallszahlen hergenommen wird.

Ist dieser Seed bei wiederholtem Abrufen der Zahlenfolge gleich, so wird auch die gleiche Zahlenfolge generiert. So auch bei folgendem Code:

Random r1 = new Random(1);
for (int i = 0; i < 5; ++i)
{
    Console.Write(r1.Next(0, 6)+ " ");
} 
//Der dazugehörige output ist und bleibt: 1 0 2 4 3

Jedoch kann es beim Debuggen hilfreich sein, immer den selben Seed zu verwenden, da so immer die gleiche Folge generiert wird.

Im folgenden sprechen wir jedoch nur noch über Random-Objekte, die mit dem parameterlosen Konstruktor erstellt werden.

2. Zeitabhängiger Seed

Um die vorhergehende Tatsache zu unterbinden, muss man einen möglichst zufälligen Seed wählen. Da Standard-PCs nicht über echte Zufallsgeneratoren verfügen, weicht man üblicherweise auf die aktuelle (Uhr-)Zeit oder Teile davon aus.

Der Standardkonstruktor von Random verwendet die Anzahl der verstrichenen Millisekunden seit Systemstart (Environment.TickCount) als Seed. Das ist in den allermeisten Fällen ausreichend gut, sofern man die folgende Falle umgeht.

3. Falle: Mehrfaches Erzeugen von Random-Objekten

Wenn eine Folge an Zufallszahlen benötigt wird, werden diese gern in einer Schleife erstellt. Was aber beim falschen Umgang zu einen Fehler führt.

for (int i = 0; i < 5; ++i)
{
    Random rnd = new Random();
    Console.Write("{0} ", rnd.Next());
}
// Gibt z.B. folgende Ausgabe: 4 4 4 4 4

Was ist an dem Code falsch?
Dies beschreibt die MSDN Dokumentation wie folgt:

Zitat von: Random-Konstruktor (System)
Der Standard-Startwert wird von der Systemuhr abgeleitet und verfügt über eine endliche Auflösung. Unterschiedliche Random-Objekte, die unmittelbar nacheinander durch einen Aufruf des Standardkonstruktors erstellt werden, verfügen daher über identische Standard-Startwerte und erzeugen so identische Sätze von Zufallszahlen.

Und da die PCs so schnell laufen, werden neue Random-Objekte in der Schleife unmittelbar nacheinander erstellt und somit ist auch die Systemzeit in sehr vielen Schleifendurchläufen dieselbe und somit auch der Seed, so dass das unter Punkt 1 erwähnte seine Auswirkung zeigt.

3.1. Warum funktionierts im Debugger?

Wenn man hingegen im Debugger Zeile für Zeile durchgeht, dann erhöht sich damit die Zeit zwischen den Schleifendurchläufen drastisch, wodurch die Random-Instanzen mit unterschiedlichen Zeiten initialisiert werden.

3.2. Lösung des Problems

Um dieses Problem zu umgehen, reicht es bereits, die Random-Instanz außerhalb der Schleife - oder noch besser nur ein mal pro Objekt (Instanzvariable) oder nochmal besser nur einmal pro Klasse (statische Variable) - zu erzeugen:

private void GenerateRandom()
{
    Random rnd = new Random();
    for (int i = 0; i < 5; ++i)
    {
        Console.Write("{0} ", rnd.Next());
    } 
}

3.2.1 Warum ist eine Instanzvariable oder statische Variable besser

Vorhergehender Code sichert ab, dass das Random-Objekt nicht in jedem Schleifendurchlauf neu erstellt wird. Jedoch kann es auch vorkommen, dass die Methode GenerateRandom() selbst in einer Schleife ausgeführt wird. Dadurch kann es passieren, dass jedes neue Random-Objekt wiederum den identischen Seed bekommt und wir wieder das Problem von Punkt 3 haben. Folgendes Beispiel soll dies verdeutlichen:


private void Test()
{
    int i = 0
    while(i++ < 5)
    {
        GenerateRandom();
    } 
}

private void GenerateRandom()
{
    Random rnd = new Random();
    for (int i = 0; i < 5; ++i)
    {
        Console.Write("{0} ", rnd.Next());
    } 
}

Hiermit haben wir exakt dasselbe Problem wie in Punkt 3, dass ein neues Random-Objekt in jedem Schleifendurchgang der while-Schleife erzeugt wird.

Durch die Deklaration und Initialisierung von rnd als Instanzvariable wird erreicht, dass das Random-Objekt zumindest im beinhaltenden Objekt nur ein Mal erstellt wird.

Das reicht aber wieder nur dann, wenn sichergestellt ist, dass die Objekte der Klasse nie in einer Schleife oder auf andere Weise schnell hintereinander erstellt werden. Deshalb ist es normalerweise noch besser, das Random-Objekt nur einmal pro Klasse zu erstellen.


private static Random rnd = new Random();
private void GenerateRandom()
{
    for (int i = 0; i < 5; ++i)
    {
        Console.Write("{0} ", rnd.Next());
    } 
}

Fazit: Man muss auf jeden Fall verhindern, dass mehrere Random-Objekte mit dem parameterlosen Konstruktor schnell hintereinander erstellt werden.

4. Threadsicheres Erstellen von Zufallszahlen

Die Random-Klasse ist nicht threadsicher, daher darf nicht mit mehreren Threads gleichzeitig auf eine Instanz von Random zugegriffen werden. Ein naiver Ansatz ist die Synchronisation des Zugriffs auf die Instanz, z.B. durch die Verwendung eines locks:


private static Random _rnd = new Random();

public int GetThreadSafeRandomNumber()
{
	lock (_rnd)
	{
		return _rnd.Next();
	}
}

Dieser Ansatz ist einfach und auch richtig, allerdings wird bei jedem Zugriff synchronisiert und das ist aus Performance-Sicht suboptimal. Je mehr Threads so Zufallszahlen erzeugen wollen, desto höher die "Resource Contention". Um dies zu vermeiden kann eine Random-Instanz pro Thread verwendet werden (anstatt einer pro Anwendungsdomäne, wie im naiven Ansatz), deren Seed z.B. durch eine anwendungsdomänenweite Random-Instanz generiert wird.


[ThreadStatic]
private static Random _rnd;

private static int _seed = Environment.TickCount;
public int GetThreadSafeRandomNumber()
{
    if (_rnd == null) 
    {
        int seed = Interlocked.Increment(ref _seed);
        _rnd     = new Random(seed);
    }
    return _rnd.Next();
}

5. Siehe auch
*Zufallszahlen, die sich nicht wiederholen *[Snippet] Zufallszahlen, die sich nicht wiederholen

Gruß,
Michael

Mein Blog
Meine WPF-Druckbibliothek: auf Wordpress, myCSharp