Fail: Timeout NEVER = ?

Ich mag Funktionen mit Timeouts, denn mit denen kann man “kalkulieren”, während andere ewig blockieren können.

Folglich baue ich auch selbst gerne solche ein.
Doch … was ist, wenn man unendlich auf ein Ereignis warten will?


Die kurze Antwort: Man kann zwei Funktionen für ein Feature implementieren. Eine mit Timeout und eine ohne. Aber das ist lästig.

An der Stelle tauchen dann (in vielen Frameworks) Konstanten auf, die die Unendlichkeit repräsentieren.
Titel wie NOTIMEOUT, INFINITE, NEVER und viele mehr sollen ausdrücken, dass man nicht eine bestimmte Anzahl von Sekunden oder Millisekunden warten soll, sondern so lange “blockieren” darf, bis die gewünschte Operation fertig ist.

Solche Timeout-Konstanten können aber Wertebereiche repräsentieren, die sich mit anderen Zeitangaben überlappen können.

Ein Klassiker unter Windows ist INFINITE welches als DWORD Typ den Wert 0xffffffff trägt.
Die meisten Warteroutinen akzeptieren einen DWORD als Millisekundenwert, somit man maximal maximal 4 294 967 295 Millisekunden (50 Tage) auf ein Ereignis warten kann.
Der INFINITE Wert lässt das System allerdings ewig warten.

Hier kann man dann den Fehler machen und das unendliche Timeout durch eine Subtraktion per Überlauf auf diesen Wert bringen.

Ein anderer Klassiker ist die falsche Portierung dieses Wertes (z.B. auf Linux).

1unsigned wait_infinite_ms = (unsigned)-1;
2struct timespec timeout = { wait_infinite_ms / 1000, wait_infinite_ms * 1000000 };
3select(... &timeout);

Es ist übrigens nicht lustig solche Fehler in anderen Programmen zu suchen, wenn die einzige Info ist:

Das Programm bleibt hängen.

-1 != NEVER

Ich bin dabei in meine eigene Falle getappt, als ich die DOS Keyboard Warteroutinen erstellt habe. Ein leichtfertiges
await_and_read_kbd(ptr_char, -1)
ließ genau (unsigned int)-1 Millisekunden Zeit verstreichen. Und auf einem 16-Bit System sind das 65 Sekunden.

Mein Problem lag in der Erwartungshaltung:
Warte unendlich lange auf Ereignis, brich bei Fehlern ab. “Unendlich” bedeutet ohne Timeout, und der Timeout-Fehler nach 65 Sekunden brach dann unerwartet die Funktion ab.

Wenn man also schon ein NEVER als Zeiteinheit zulässt, dann muss man diesen Spezialfall auch ausprogrammieren.

wait, wait_for, wait_until

C++ löst dieses Problem aus meiner Sicht sehr schön, indem es nicht auf eine API mit magischen Konstanten setzt, sondern in vielen Bereichen 3 Warte-Varianten anbietet.

  • wait() wartet unendlich
  • wait_for() wartet eine fixe Zeitspanne ab
  • wait_until() wartet bis ein übergebener Zeitpunkt erreicht ist.

Eine solche Lösung schwebt mir daher in meinem C++-Layer auch vor.

Fazit

Man spielt nicht mit der Zeit, schon gar nicht mit der Unendlichkeit.

Tatsächlich sind mir Endlosschleifen wegen falscher Zeit und Warteroutinen schon öfter untergekommen. Die Fixes sind dann in wenigen Zeilen durchgeführt, aber das Auffinden solcher Schwachstellen ist mühsam.

Es macht also Sinn, sich vorher über eine “gute API” den Kopf zu zerbrechen und dieses Schema einheitlich projektweit umzusetzen, als auf Bugs und Inkonsistenzen zu warten.

Leider spielen da auch viele Hilfsbibliotheken nicht gut mit, die Warteroutinen auch alle etwas anders ausgestalten.