Hallo Besucher, der Thread wurde 1,6k mal aufgerufen und enthält 15 Antworten

letzter Beitrag von Zirias/Excess am

Stabiler Timer bzw. Taktgenerator (Windows/Linux)

  • Ich habe mir für mein Projekt einen Timer geschrieben der qausi einen Quartz darstellt. Die Idee ist dass man der Klasse sagt welche Frequenz man haben will (z.B. 60 Hz) und dann bekommt man eben 60 Ticks pro Sekunde. Das Problem dabei ist aber, dass durch das Scheduling der Threads diese Taktung nicht sehr genau ist. Wenn ich z.B. das 10 Sekunden lange laufen lasse, dann sollte ich also 600 Takte erhalten habe. Tatsächlich vergehen dafür aber of bis zu 13 Sekunden als Maximum, (häufig sind es 11 Sekunden). Jetzt habe ich mich gefragt wie die Emulatorenentwickler das machen. Wenn die ein System emulieren, dann müssen die doch einen recht stabilen Takgenerator haben, damit das emulierte System mit der korrekten Geschwindigkeit laufen kann.


    Möglicherweise ist diese Frage auch besser in einem der Programmier Boards aufgehoben, war mir da nicht sicher wo das besser hinpasst.

  • Wenn das Scheduling keine expliziten zeitgenauen Unterbrechungen vorsieht, muss das System so stark beschleunigt werden, dass das Event mindestens einmal im gewünschten Intervall ausgeführt wird, und dann muss der Start des nächsten Intervalls abgewartet werden, bevor der nächste Versuch stattfindet. Das kann ein zwei- bis dreistelliges Vielfaches der Originalgeschwindigkeit sein.

    Schönes Beispiel ist der Pi1541. Wieviel MHz hat der 3B, die benötigt werden, um eine 1MHz-Floppy zu emulieren? Da muss die Emulation innerhalb jedes Real-Takts intern den Zustand aller Bauteile richtig umstellen.

  • Ich habe das in C++ implementiert. Die Grundidee war nanosleep(1000000/60) ausführen (bei 60 Hz). Wenn der Sleep zurückkommt dann schicke ich ein Signal an den Thread der darauf wartet und der sleep geht weiter. Das ist natürlich nicht perfekt, weil ja das ausführen des usleep() einen gewissen Overhead hat, aber das habe ich so in etwa im Griff. Allerdings scheint das eben durch das Scheduling da zwischendurch das zu lange zu dauern.


    Die Implementierung habe ich hier:

    https://github.com/skeetor/chr…rdware/IntervallDelay.cpp


    Mit der sync() Funktion habe ich versucht auf das Ende der Sekunde aufzusynchronisieren um den Drift durch das Scheduling auszugleichen, aber das funktioniert auch nicht so wie geplant. Ich dachte, dass ich 59 Takte lang mit nanosleep warte und den letzten Takt dann bis zum Ende der Sekunde polle. Dann sollte eigentlich der Drift relativ gering bleiben.


    Hoffe das isthalbwegs verständlich. :)


    Ein Testcase ist hier (PeriodicTimer):

    https://github.com/skeetor/chr…Hardware/TestHardware.cpp

  • Sämtliche sleep()-Funktionen garantieren immer nur, dass mindestens die angegebene Zeit gewartet wird, mit sowas lässt sich Drift also gar nicht verhindern.

    Nimm stattdessen sowas wie next_alarm_time = previous_alarm_time + cycle_time, dann gleichen sich die Fehler mit der Zeit aus. Problematisch wird es allerdings, wenn die Maschine wirklich zu langsam ist, das muss man extra abfangen: Wenn beim Setzen der nächsten Alarmzeit diese bereits in der Vergangenheit liegt, kann man eigentlich nur noch per Debug-Ausgaben dem Benutzer Bescheid sagen und einen Alarm ausfallen lassen...

  • Je nachdem, welcher Jitter noch ok ist, kann es auch bereits reichen, sich eine hinreichend genaue Zeit geben zu lassen (Linux: gettimeofday - das hat immerhin Microsekundenauflösung). Und danach die notwendige Wartezeit berechnen, statt konstant annehmen. So zieht man in die Ausführung ein, dass man ggf. verspätet dran ist und wartet dann weninger.

  • (zu Post #5)

    Das ist ja dann schon 'viel' zu langsam. Im Prinzip fängt es doch schon an, kritisch zu werden, wenn die Wartezeit bis zum nächsten Event Null ist. Dann muss man den ja gerade so 'on time' durchführen oder leicht nachholen.

  • Sämtliche sleep()-Funktionen garantieren immer nur, dass mindestens die angegebene Zeit gewartet wird, mit sowas lässt sich Drift also gar nicht verhindern.

    Ja. Deshalb ja auch der letzte Takt der dann auf das Ende der Sekdunde warten soll statt zu sleepen.

    Zitat

    Nimm stattdessen sowas wie next_alarm_time = previous_alarm_time + cycle_time, dann gleichen sich die Fehler mit der Zeit aus. Problematisch wird es allerdings, wenn die Maschine wirklich zu langsam ist, das muss man extra abfangen: Wenn beim Setzen der nächsten Alarmzeit diese bereits in der Vergangenheit liegt, kann man eigentlich nur noch per Debug-Ausgaben dem Benutzer Bescheid sagen und einen Alarm ausfallen lassen...

    Der Einfachheit halber gehe ich erstmal davon aus dass die Maschine schnell genug ist. :) Aber wie kann ich bei dieser Methode garantieren dass der Takt stabil bleibt?


    Je nachdem, welcher Jitter noch ok ist, kann es auch bereits reichen, sich eine hinreichend genaue Zeit geben zu lassen (Linux: gettimeofday - das hat immerhin Microsekundenauflösung). Und danach die notwendige Wartezeit berechnen, statt konstant annehmen. So zieht man in die Ausführung ein, dass man ggf. verspätet dran ist und wartet dann weninger.

    So hatte ich das auch angedacht gehabt, das ist jauch genau die Idee welche die sync() Funktion hat. Allerdings will ich ja nicht CPU Zeit verbraten indem ich eine busywait durchführe. Deshalb würde ich ja die Zeit "dazwischen" mit sleep überbrücken. Ich bin davon ausgegangen dass der zusätzliche Overhead halbwegs im Rahmen bleibt.

    Wenn ich mir also die Zeit mit gettimeofday hole, mit hinreichender Gnauigkeit ms reichen da, wa mache ich dann in der restlichen Zeit?

  • Wenn ich mir also die Zeit mit gettimeofday hole, mit hinreichender Gnauigkeit ms reichen da, wa mache ich dann in der restlichen Zeit?

    Nützt nichts, in dem Aufruf ist das nunmal in µs drin. "pthread_mutex_timedlock" wäre einen Versuch wert - wenn es usleep und Varianten nicht tun.

    Ansonsten: Priorität von dem Prozess erhöhen - bitfalls bis zu Real Time Prio. Und wenn das alles nichts hilft: Preemptiven Kernal verwenden ("RT [Real Time] Patches"). Die sorgen dafür, wenn ein Prozess mit höherer Priorität da ist und bereit, dass dann sofort die CPU gegeben wird.

  • Wie Mac Bacon schon schrieb: Man gibt einen Zielwert vor für die Zeit, führt die gewünschte Aktion aus (in der Regel Emulation eines Frames) und vergleicht die neue Zeit mit der gewünschten Zeit. Ist die aktuelle Zeit zu niedrig, legt man das Programm für die Restdauer schlafen. Ist schon zu viel Zeit verstrichen, wird der Zielwert angepaßt auf die aktuelle Zeit, ansonsten wird für den neuen Zielwert nur eine Differenz zum alten addiert. In etwa so:

    Nebenbei: Auch beim schnellsten Prozessor kann es passieren, daß die aktuelle Zeit weit über der gewünschten liegt. Das ist z. B. dann der Fall, wenn ein Benutzer das Fenster verschiebt. Dabei wird der Thread automatisch lahmgelegt und erst weiter fortgeführt, wenn der Benutzer den Finger vom Mausbutton genommen hat (s. z. B. WinVice oder WinUAE).

  • Wie Mac Bacon schon schrieb: Man gibt einen Zielwert vor für die Zeit, führt die gewünschte Aktion aus (in der Regel Emulation eines Frames) und vergleicht die neue Zeit mit der gewünschten Zeit. Ist die aktuelle Zeit zu niedrig, legt man das Programm für die Restdauer schlafen. Ist schon zu viel Zeit verstrichen, wird der Zielwert angepaßt auf die aktuelle Zeit, ansonsten wird für den neuen Zielwert nur eine Differenz zum alten addieren

    Alles klar. Das hatte ich beim ersten Mal nicht ganz verstanden. :)In meinem Konzept habe ich das andershrum gedacht. ich habe einen "Quartz" der mir regelmäßige Takte liefert. Bei jedem Tak wird der aktuelle Bidlschirminhalt angezeigt, egal was da grade drin ist. Wenn der Thread mit dem Zeichnen noch nciht fertig war hat er Pech gehabt. Allerdings ist dieser Taktgeber eben nur für den Bildschirmrefresh zuständig und nicht für die ganze Maschine (obwohl man die Klasse dafür genauso verwenden könnte).

    Nebenbei: Auch beim schnellsten Prozessor kann es passieren, daß die aktuelle Zeit weit über der gewünschten liegt. Das ist z. B. dann der Fall, wenn ein Benutzer das Fenster verschiebt. Dabei wird der Thread automatisch lahmgelegt und erst weiter fortgeführt, wenn der Benutzer den Finger vom Mausbutton genommen hat (s. z. B. WinVice oder WinUAE)

    Gut, das sehe ich aber als kein grundsätzliches Problem an. Das entspricht dann ja auch in etwa der Situation das die Emulation pausiert werden könnte. Aber ist klar, dass man das berücksichtigen muss, damit dann wieder alles weiterläuft wie erwartet.

  • Die Grundidee war nanosleep(1000000/60) ausführen (bei 60 Hz). Wenn der Sleep zurückkommt dann schicke ich ein Signal an den Thread der darauf wartet und der sleep geht weiter.

    Den Thread gerade erst entdeckt, und wahrscheinlich ist das gar nicht mehr relevant? Trotzdem kurz dazu: Diese Idee ist sehr ineffizient und hängt sehr stark vom Scheduler ab.


    Einen periodischen Timer musst du wohl mit systemspezifischen Mitteln bauen. POSIX bietet hier schon genau das richtige Werkzeug: https://linux.die.net/man/2/setitimer -- damit bekommst du periodisch ein SIGALRM, und zwar bei Verwendung von ITIMER_REAL auch in "echter" Systemzeit. Es wird nicht jedes Signal exakt "on time" behandelt werden, aber "ungefähr" 60 Hz so dass es im Mittel auch stimmt sollte auf einem auch nur halbwegs modernen Rechner problemlos machbar sein.


    Auf Windows weiß ich gerade nichts passendes, aber wahrscheinlich gibt es auch da etwas.


    Wenn du natürlich ein Timing willst das eher in Richtung Cycles eines emulierten Prozessors geht, dann wird auf den meisten Systemen die Genauigkeit irgendwelcher timer nicht ausreichen, aber dazu wurde hier im Thread ja schon geschrieben :)

  • POSIX bietet hier schon genau das richtige Werkzeug: https://linux.die.net/man/2/setitimer

    Kleine Rückfrage dazu: Signale werden soweit ich weiß an Prozesse und nicht an Threads geschickt. Inwiefern kann das Probleme machen (kein besonderer Grund für die Frage, jedoch ist das gerade eine Gelegenheit, den Werkzeugkasten auszubauen..)?

  • Signals und Threads sind eine etwas komplizierte Geschichte -- es gibt allerdings auch pro Thread eine "signal mask", die man mit pthread_sigmask() setzen kann. Allgemein wird für multi-threaded Anwendungen empfohlen, Signale in allen bis auf einen Thread zu blocken, dieser Thread ist dann ausschließlich dafür zuständig, sie zu verarbeiten.


    Wenn man das nicht tut ist es soweit ich weiß nicht definiert, welcher Thread den signal handler ausführt, und das kann wirklich ein Problem sein :)

  • Mir ist gerade noch eingefallen, dass ich vor inzwischen 5 Jahren mal einen periodischen Timer gebraucht habe, für ein kleines Spielchen, das ich eigentlich geschrieben habe, um das curses API zu lernen ;) Da ich das für POSIX, Windows, MS-DOS protected mode und MS-DOS real mode gebaut habe gibt's für den Timer 4 verschiedene Implementierungen...


    POSIX: https://github.com/Zirias/curs…c/platform/posix/ticker.c

    Windows: https://github.com/Zirias/curs…c/platform/win32/ticker.c


    Die Timer sind jeweils so gebaut, dass das Hauptprogramm auf den nächsten "tick" wartet ... wenn man währenddessen was tun will muss man das natürlich anders bauen, aber das dürfte kein zu großes Problem sein:


    - auf POSIX einfach Code in den signal handler schreiben

    - auf Windows kann man SetWaitableTimer() eine "completion routine" mitgeben, die bei Ablauf aufgerufen wird