Ein Test schlägt einmal in zwanzig Durchläufen fehl. Die Testdaten sind jedes Mal andere, also lässt sich der Fehler nicht wiederholen und damit auch nicht untersuchen. Was hier fehlt, ist nicht besserer Zufall, sondern derselbe Zufall ein zweites Mal.
Im Bild erzeugt derselbe Startwert zweimal dieselbe Zahlenreihe. Genau das ist die Aufgabe dieser Funktion, und sie hat mit der Qualität des Zufalls nichts zu tun.
Was PHP mt_srand() macht
PHP mt_srand() setzt den Startwert des Zufallsgenerators. Alles, was danach an Zufallszahlen erzeugt wird, ergibt sich rechnerisch aus diesem einen Wert. Setzt man ihn erneut auf dieselbe Zahl, beginnt die Folge von vorn, Wert für Wert identisch.
<?php
mt_srand(42);
echo mt_rand(1, 100), ' ', mt_rand(1, 100), ' ', mt_rand(1, 100);
// 64 30 72
mt_srand(42);
echo mt_rand(1, 100), ' ', mt_rand(1, 100), ' ', mt_rand(1, 100);
// 64 30 72
Beide Durchläufe liefern dieselben drei Zahlen, weil PHP mt_srand() den Generator zweimal in denselben Ausgangspunkt versetzt hat. Hier geht es also nicht darum, möglichst unvorhersehbare Werte zu bekommen, sondern darum, dieselben Werte erneut zu erhalten. Wer echten Zufall für Auswahl und Anzeige braucht, findet die passenden Wege in Zufällige Werte aus einem Array auswählen.
Die Signatur im Original
Die englische Beschreibung ist kurz und enthält trotzdem den Hinweis, der die häufigste Frage beantwortet:
mt_srand(int $seed = 0, int $mode = MT_RAND_MT19937): void
/* seed - an arbitrary integer seed value
mode - MT_RAND_MT19937 uses the fixed, correct Mersenne
Twister implementation, MT_RAND_PHP uses the
incorrect implementation that PHP shipped before 7.1
return- no return value
Seeds the random number generator. As of PHP 7.1.0 the random
number generator is seeded automatically, so there is usually
no need to call this function. It is only needed when a
reproducible sequence of values is required. */
Der letzte Satz räumt mit einem verbreiteten Muster auf. Ein Aufruf zur Verbesserung des Zufalls ist seit PHP 7.1 überflüssig, und in der Regel sogar schädlich.
Wie ein Pseudozufallsgenerator arbeitet
Um zu verstehen, warum PHP mt_srand() diese Wirkung hat, hilft ein Blick auf das Verfahren darunter. Der Generator hinter mt_rand() heißt Mersenne Twister und ist ein Rechenverfahren, kein Würfel. Er hält einen internen Zustand, gibt daraus einen Wert aus und verändert dabei den Zustand für den nächsten Aufruf.
flowchart TD
A[mt_srand mit Startwert] --> B
B[Interner Zustand] --> C[mt_rand aufrufen]
C --> D[Wert zurueckgeben]
C --> E[Zustand fortschreiben]
E --> B
A --> F[Gleicher Startwert]
F --> G[Gleiche Folge]
Weil der Zustand mit jedem Aufruf weiterrückt, kommt es auf die Reihenfolge an. Schiebt sich irgendwo ein zusätzlicher Aufruf dazwischen, verschiebt sich alles danach.
<?php
mt_srand(7);
$a = mt_rand(1, 6);
$b = mt_rand(1, 6); // 3, 5
mt_srand(7);
$x = mt_rand(1, 6);
$hilfswert = mt_rand(1, 99); /* dieser Aufruf verschiebt alles */
$y = mt_rand(1, 6); // 3, dann 4 statt 5
Das ist der Grund, warum reproduzierbare Abläufe keine bedingten Zufallsaufrufe enthalten sollten. Ein if, das manchmal eine Zahl zieht und manchmal nicht, macht die Wiederholbarkeit zunichte.
Muss ich vor jedem mt_rand initialisieren?
Nein, und in altem Code steht genau deshalb oft etwas Schädliches. Vor PHP 7.1 kursierte das Muster, vor jedem Aufruf mit der aktuellen Zeit zu initialisieren. Seit PHP 7.1 initialisiert PHP den Generator beim ersten Bedarf selbst, und zwar aus einer geeigneten Quelle des Betriebssystems.
<?php
/* So stand es frueher haeufig im Code.
Die Zeit ist erratbar, also wird die Folge
dadurch schlechter statt besser. */
mt_srand(time());
$code = mt_rand(100000, 999999);
/* Heute genuegt der blosse Aufruf. */
$code = mt_rand(100000, 999999);
PHP mt_srand() gehört also nur dann in den Code, wenn Wiederholbarkeit ausdrücklich gewünscht ist. In jedem anderen Fall ist der Aufruf bestenfalls wirkungslos.
srand() oder mt_srand(): der Unterschied
Beide Funktionen stehen nebeneinander im Handbuch, und die Frage nach dem Unterschied taucht regelmäßig auf. Die Antwort ist seit PHP 7.1 unspektakulär: Es gibt keinen. srand() ist ein anderer Name für dieselbe Funktion, ebenso wie rand() und mt_rand() denselben Generator ansprechen.
<?php
srand(2026);
echo mt_rand(1, 1000); // 373
mt_srand(2026);
echo rand(1, 1000); // 373
Vor PHP 7.1 waren das tatsächlich zwei getrennte Generatoren mit unterschiedlichen Verfahren. Wer altem Code begegnet, in dem beide gemischt verwendet werden, kann das heute gefahrlos vereinheitlichen. Für neuen Code ist PHP mt_srand() die Schreibweise, die den Generator beim Namen nennt.
Der zweite Parameter und die alte Implementierung
Der zweite Parameter von PHP mt_srand() existiert aus einem einzigen Grund: PHP hatte den Mersenne Twister bis Version 7.1 fehlerhaft umgesetzt. Die Korrektur veränderte die erzeugten Folgen, was für alle problematisch war, die auf bestimmte Folgen angewiesen waren.
| Konstante | Verfahren | Wann |
MT_RAND_MT19937 | korrekt | Voreinstellung, für allen neuen Code |
MT_RAND_PHP | fehlerhaft, wie vor 7.1 | nur um alte Folgen nachzustellen |
<?php
/* Nur fuer den Fall, dass eine Folge aus einem alten
System exakt nachgestellt werden muss. */
mt_srand(12345, MT_RAND_PHP);
echo mt_rand(1, 1000);
Diese Konstante ist ein Werkzeug für Altbestände und keine Wahlmöglichkeit. Wer sie in neuem Code findet, sollte nachfragen, warum sie dort steht.
Praxis: einen fehlgeschlagenen Lauf wiederholen
Das eingangs beschriebene Problem löst sich, sobald der Startwert bekannt ist. PHP mt_srand() bekommt ihn dann nicht mehr zufällig, sondern aus der eigenen Hand. Statt ihn dem Zufall zu überlassen, erzeugt man ihn selbst, schreibt ihn ins Protokoll und arbeitet mit ihm weiter.
<?php
$startwert = (int) ($_ENV['TEST_SEED'] ?? random_int(1, PHP_INT_MAX));
mt_srand($startwert);
error_log('Testlauf mit Startwert ' . $startwert);
$daten = [];
for ($i = 0; $i < 1000; $i++) {
$daten[] = [
'menge' => mt_rand(1, 500),
'preis' => mt_rand(100, 99999) / 100,
];
}
Schlägt der Lauf fehl, steht der Startwert im Protokoll. Mit gesetzter Umgebungsvariable entsteht exakt derselbe Datensatz erneut, und der Fehler lässt sich untersuchen statt zu jagen.
Praxis: aus einem Schlüssel eine feste Folge ableiten
Manchmal soll dieselbe Eingabe immer zum selben Ergebnis führen, ohne dass eine Zuordnung gespeichert wird. Ein Kartengenerator, der aus einer Kartennummer immer dieselbe Landschaft baut, arbeitet nach diesem Prinzip.
<?php
function testname(string $schluessel): string
{
$vornamen = ['Anna', 'Bernd', 'Clara', 'Dirk', 'Eva'];
$nachnamen = ['Meier', 'Schulz', 'Krause', 'Weber'];
mt_srand(crc32($schluessel));
return $vornamen[mt_rand(0, 4)] . ' ' . $nachnamen[mt_rand(0, 3)];
}
echo testname('kunde-4711'); // immer derselbe Name
echo testname('kunde-4711'); /* und beim naechsten Mal auch */
Zwei Einschränkungen gehören dazu. crc32() ist eine Prüfsumme und keine Sicherheitsfunktion, die Zuordnung ist also nachvollziehbar. Und wer diesen Startwert setzt, verändert den Generator für den gesamten weiteren Ablauf, was in einem langlaufenden Skript unerwünschte Nebenwirkungen hat.
Betroffen sind nämlich nicht nur mt_rand(). Auch shuffle(), str_shuffle() und array_rand() greifen auf denselben Generator zu, weshalb ein vorher gesetzter Startwert auch ihre Ergebnisse festlegt.
<?php
$aufgaben = ['Bruchrechnen', 'Prozent', 'Dreisatz', 'Zinsen'];
mt_srand(99);
shuffle($aufgaben);
print_r($aufgaben);
// Prozent, Zinsen, Bruchrechnen, Dreisatz
mt_srand(99);
shuffle($aufgaben);
print_r($aufgaben);
// dieselbe Reihenfolge wie eben
Das ist der Weg für eine Aufgabenreihenfolge, die sich bei einer Wiederholung genauso zeigt wie beim ersten Mal. Ohne den gesetzten Startwert mischt shuffle() bei jedem Aufruf neu.
Wofür PHP mt_srand() ausdrücklich nicht taugt
Ein gesetzter Startwert macht jede daraus erzeugte Folge vorhersagbar. Das ist der Zweck der Funktion und zugleich der Grund, warum sie in sicherheitsrelevantem Code nichts verloren hat.
<?php
/* Falsch: wer den Startwert erraet, kennt jedes Token. */
mt_srand(time());
$token = bin2hex(pack('N', mt_rand()));
/* Richtig: kryptografisch sichere Quelle, kein Startwert. */
$token = bin2hex(random_bytes(16));
$code = random_int(100000, 999999);
Für Sitzungsschlüssel, Passwortrücksetzungen, Einmalcodes und alles, was jemand erraten könnte, gelten random_int() und random_bytes(). Diese Funktionen kennen bewusst keinen Startwert, weil Wiederholbarkeit dort ein Fehler wäre. Wie sich eindeutige Kennungen sauber erzeugen lassen, zeigt PHP uniqid zum Erzeugen und richtigen Einsetzen eindeutiger IDs.
Fazit
PHP mt_srand() ist kein Werkzeug für besseren Zufall, sondern für wiederholbaren. Seit PHP 7.1 initialisiert PHP den Generator von selbst, ein Aufruf mit der aktuellen Zeit verschlechtert das Ergebnis eher. srand() ist seither derselbe Aufruf unter anderem Namen.
Zwei Muster tragen in der Praxis: den Startwert protokollieren, um einen fehlgeschlagenen Lauf exakt zu wiederholen, und ihn aus einem fachlichen Schlüssel ableiten, wenn dieselbe Eingabe immer dasselbe Ergebnis liefern soll. Und eine Grenze gilt ohne Ausnahme: Sobald jemand den erzeugten Wert nicht erraten dürfen soll, ist random_int() die richtige Wahl und nicht der Weg über einen Startwert.