Zykluszeit von Pythonprogramm in Windows

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
stefanf
User
Beiträge: 3
Registriert: Donnerstag 25. Juli 2019, 09:14

Hallo zusammen,

ich bin neu hier in dem Forum und arbeite auch noch nicht so lange mit Python und hoffe ihr könnt mir weiterhelfen.

Momentan schreibe ich einen OPC UA Client um Prozessdaten aus einer Maschine zu lesen und weiterverarbeiten zu können. Dazu habe ich zwei Klassen erstellt: NodeInterface und ClientInterface. Die NodeInterface Klasse beinhaltet die Methoden zum lesen der Variablen und Werte vom OPC UA Server. Die ClientInterface Klasse soll die gelesenen Daten vom Nodeinterface an ein Anwenderprogramm (exisitiert bisher noch nicht und muss für meine Aufgabe momentan noch nicht berücksichtigt werden) weitergeben und auch die Daten in einem großen Array zwischenspeichern. Die ClientInterface Klasse arbeitet als Thread, da später mehrere Anwenderprogramme "parallel" laufen sollen. Über eine while-Schleife wird im ClientInterface eine Methode des NodeInterface aufgerufen, um die Prozessdaten über den OPC UA Server aus der Maschine zu lesen. Die Maschine lädt in harter Echtzeit in einem Takt von 1 ms neue Daten auf den OPC UA Server hoch und erst wenn neue Daten auf dem Server sind fängt mein Client an diese auszulesen (Da der Client langsamer ist als der 1 ms Takt der Maschine und nicht in harter Echtzeit arbeitet, musste ich noch einen Ringpuffer dazwischen schalten).

Jetzt zu meinem Problem:
Ich habe nun die Zykluszeiten meines Clients über einen bestimmten Zeitraum gemessen, um zu gucken wie lange der Client für einen Aufruf braucht und so eventuellen Datenverlust auszuschließen. Dabei benötigt der Client im Durchschnitt 2,5 ms für einen Aufruf. Jedoch gibt es ca. 1,5 mal pro Minute einen Ausreißer von ca. 250 bis 280 ms. Meine Vermutung war, dass dies an dem Garbage Collector von Python liegt, doch ein Deaktivieren und manuelles regelmäßiges Ausführen vom GC oder die Anpassung der GC threshold hat auf diese Ausreißer keine Auswirkung.

Meine Frage:
Habt ihr eine Idee woran dies liegen könnte? Ist dies ein Python Problem oder liegt das eher an Windows (dass Windows mein Programm unterbricht, um Systemeigene Prozesse auszuführen. Habe deswegen schon die Prio von meinem Programm auf Echtzeit gestellt -> keine Auswirkung :/)? Und wie könnte man diese Ausreißer verhindern?

Ich hoffe ich habe keine wichtigen Infos vergessen, um mein Problem zu verstehen.
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Grundsaetzlich ist das erstmal ein OS-Thema. Denn insbesondere wenn du den GC deaktiviert hast ist der Rest basierend auf reference counting deterministisch genug. Wenn es dann also trotzdem in gleicher Frequenz zu den Problemen kommt (selbst wenn der Speicher bestaendig ansteigt, das kann man ja erstmal zum testen hinnehmen), dann ist der GC nicht schuld.

Mir fehlen hier noch viele Details wie zB was de OPC UA Server genau ist, wie der den harten 1ms Takt haelt (wenn er auch unter Windows laeuft). Und ich wuerde auch massiv in Frage stellen, dass du Threading brauchst. Das mehrere Clients eine Verbindung haben beruehrt nicht den Entwurf. Es gibt genug Server wie zB NGINX die 10tausende Verbindungen aufhalten und bedienen - mit genau EINEM Event-Loop/Thread. Mehrere davon in Python lauefen eh nicht konkurrierend, und verlangen im Zweifel sogar mehr Ressourcen und verlangsamen Dinge. Mir ist auch nicht klar, wo genau der Ringpuffer lebt.

Grundsaetzlich kann Python natuerlich trotzdem zum Problem werden, wenn es schlicht zu langsam ist die geforderten 1ms an Verarbeitungszeit zu erreichen. Generell ist eine so harte Taktung fuer ein nicht-RT-OS und dann auch noch eine ganze Reihe von per IPC kommunizierenden Komponenten unrealistisch. Das funktioniert nur, wenn die eigentlich geringe Datenrate (1KHz von wahrscheinlich bestenfalls ein paar kB) durch intensive Bursts der Verarbeitung dann am Ende doch schneller verarbeitet wird als sie reinkommen. Da kommt also die Geschwindigkeit der Implementierung, und dadurch die von Python, schon durchaus mit rein. Aber das ALLES schoen im 1KHz-Takt die Daten konsumiert, verarbeitet und weiterschickt - das wird schwierig bis unmoeglich.
stefanf
User
Beiträge: 3
Registriert: Donnerstag 25. Juli 2019, 09:14

Danke für deine schnelle Antwort.

Die Maschinen Steuerung läuft über TwinCat und stellt gleichzeitig die harte Echtzeit mit dem 1 kHz Takt auf einem eigenen reservierten Kern. Der OPC UA Server ist ein Erweiterungspaket für TwinCat und ist über ADS mit der Steuerung verbunden, sodass die Daten aus der Steuerung in Echtzeit auf den Server geladen werden können. Server und Steuerung bilden quasi eine eigene in TwinCat interne Umgebung mit Echtzeit.

Mein Gedanke hinter dem Threading war, dass ich am Ende wahrscheinlich mindestens 2 Arten von Anwenderprogrammen habe, die parallel die Daten verarbeiten soll. Das eine Programm kann dabei zusammen mit dem Zyklus vom Client aufgerufen werden, während das zweite Programm eher nicht zyklisch arbeitet und die Daten nur auf Abruf benötigt (daher das sammeln der Daten vom Client in einem großen Array). Ist Threading für diesen Fall dann trotzdem nicht notwendig, um zu garantieren, dass das zyklische Programm keine Daten verliert?

Der Ringbuffer (puffert momentan 100 ms an Daten) wird in TwinCat in der Echtzeitumgebung erstellt, im 1 kHz Takt mit Daten befüllt und auf dem Server aktualisiert. Mein Client liest quasi nur noch den Ringbuffer vom Server aus und verwendet die Daten an der Stelle wo er zuletzt aufgehört hat zu lesen.

Die harte Taktung von 1 kHz im Client mit Python zu programmieren ist sehr schwer oder unmöglich, da stimme ich dir zu. Daher kann ich über den Ringbuffer gleich mehrere Daten auslesen und so Zeit sparen, vorallem da die Datenmenge nicht allzu groß ist (ca. 20 kb für einen komplett gefüllten Puffer). Bisher kommen so meine Daten am Client auch ohne Verluste an. Lediglich wenn mein Programm statt der 2,5 ms plötzlich 250 ms braucht verliere ich ein paar Daten. Danach fängt sich der Client wieder und ich habe keine Verluste.
Zuletzt geändert von stefanf am Donnerstag 25. Juli 2019, 12:09, insgesamt 1-mal geändert.
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ok, ich verstehe schon etwas besser. Du hast also quasi ein Teilzeit-Echtzeit-System ;)

Ich wuerde zuerstmal versuchen, das Python-Programm so weit wie nur irgend moeglich abzuspecken, und schauen, ob das das Verhalten beeinflusst. Leider kenne ich mich mit Windows nicht genug aus, unter Linux kann man mit perf den Scheduler des OS profilen, und so zB sehen, dass der Prozess pre-empted wurde weil das System etwas anderes fuer wichtiger erachtet hat. Ob so etwas unter Windows existiert muesstest du mal recherchieren.

Auch eine alternative (und so dumm wie moegliche) Implentierung zB in C++ kann helfen, dem Problem auf die Schliche zu kommen.

Und bezueglich deiner beiden Clients: wo ist der Unterschied? Du hast natuerlich mehrere Verbindungen, und jede von denen hat jeweils ihre Queue an Daten. Im Grunde bietest du ja nur selbst mehrere Ringbuffer an, die dann von den jeweiligen Clients konsumiert werden. Dabei kommt aber die Frequenz der abfragen ueberhaupt nicht mit Threads ins Spiel. Wenn der Client fragt, bekommt er, was seit dem letzten mal aufgelaufen ist. Das ist dann halt unterschiedlich oft. Warum sollte ein Thread das besser machen?
stefanf
User
Beiträge: 3
Registriert: Donnerstag 25. Juli 2019, 09:14

Das sind gute Tipps, da werde ich mich mal schlau machen, danke!

Ich glaube ich verstehe was du meinst mit den Threads. Dann ist in meinem Fall Threading gar nicht nötig. Ich glaube ich habe bisher in die falsche Richtung gedacht, dass meine Schnittstelle immer die Clients aufruft und nicht, dass der Client bei meinem Programm nach Daten fragt. Macht es, denke ich, einfacher :)
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Womöglich helfen hier auch Greenlets weiter:
A “greenlet” (...) is a still more primitive notion of micro-thread with no implicit scheduling; coroutines, in other words. This is useful when you want to control exactly when your code runs. You can build custom scheduled micro-threads on top of greenlet; however, it seems that greenlets are useful on their own as a way to make advanced control flow structures.
https://pypi.org/project/greenlet/

Das Paket ist direkt nutzbar oder indirekt in Verbindung gevent. Zu letzterem findest du hier eine Einführung.
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

stefanf hat geschrieben: Donnerstag 25. Juli 2019, 12:27 Ich glaube ich habe bisher in die falsche Richtung gedacht, dass meine Schnittstelle immer die Clients aufruft und nicht, dass der Client bei meinem Programm nach Daten fragt. Macht es, denke ich, einfacher :)
Und ist ja meistens auch das übliche Vorgehen... ;)
Benutzeravatar
__blackjack__
User
Beiträge: 13071
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@snafu: Braucht man greenlets noch wo Python doch selbst schon Coroutinen über ``async``/``await`` unterstützt?
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Selbst wenn es PUSH waere - auch wenn das die schlechtere Vorgehensweise ist - auch das geht ja mit zB einem mainloop und Timern. Immer noch besser als sich in multi-threading zu verheddern. Und wie __blackjack__ schreibt, asyncio ist die Art wie man das dieser Tage macht, *wenn* die Verbindung zum OPC Server damit vereinbar ist.
Benutzeravatar
__blackjack__
User
Beiträge: 13071
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wobei man bei `asyncio` auch nicht um Threads drum herum kommt, sofern man nicht tatsächlich nur mit Sockets arbeitet. Ich find ``async``/``await`` doof bis unbrauchbar und die ”Festlegung” auf `asyncio` weil's eben in der Standardbibliothek ist, auch nicht so doll. Da hätte ich lieber gesehen, das es erst einmal ausserhalb der Standardbibliothek konkurrierende Bibliotheken gibt und sich davon eine dann durchsetzt. Ich sehe zum Beispiel keinen offensichtlichen Grund warum `curio` nicht den Weg in die Standardbibliothek hätte finden können/sollen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich komme sehr gut um Threads rum zB in einer Qt-Anwendung, der ich mit asyncqt den asyncio-mainloop untergeschoben habe. Und finde async/await gut, wenn auch nicht perfekt - gelegentlich faellt man halt mangels statischer Typen doch rein, weil's unter der Haube eben doch nur generatoren sind. Und was ich VIEL wichtiger als Perfektion finde ist, dass es jetzt endlich das Konzept eines solchen Mainloops gibt. Und zwar fuer alle, im Sprachkern bzw. der Standardbibliothek. Das wiegt fuer mich viel schwerer als das ein oder andere Feature das einem fehlen mag, die kommen schon noch.
Benutzeravatar
__blackjack__
User
Beiträge: 13071
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@__deets__: Dann muss man sich aber wahrscheinlich auch sehr auf Qt einlassen, oder eben nix machen wofür man Threads braucht. Also grössere Dateien verarbeiten zum Beispiel – entweder mit den Qt-Klassen oder eben doch wieder Threads. Hatten wir ja neulich hier im Forum: Datei zeilenweise asynchron mit dekodieren lesen ist bei Qt nur mit Kodierungen einfach, die ein Byte pro Zeichen haben. UTF-8 wird schon aufwändig. Oder ich habe etwas übersehen. Das geht in Python einfacher, asynchron dann aber nicht ohne Thread.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich habe sowas noch nicht machen muessen, aber du kannst natuerlich einen asychronen Task starten, und der dekodiert dann haeppchenweise, und delegiert dann wieder an den mainloop. Ggf. mit timer, oder einfach nur so, um anderen den Vorzug zu geben. Klar ist das invasiv bezueglich deines Codes. Wobei generatoren zu schreiben in meinen Augen immer noch ziemlich fluessig ist. Und es ist ja nicht so, als ob Threading und dessen Synchronisation etc. fuer lau kommen.
Pythonstudent
User
Beiträge: 1
Registriert: Samstag 26. November 2022, 16:48

Kann man das TwinCat-Projekt und Pythoncode irgendwo einsehen? Das würde mich sehr intressieren!
Benutzeravatar
DeaD_EyE
User
Beiträge: 1017
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

__blackjack__ hat geschrieben: Donnerstag 25. Juli 2019, 14:10 Wobei man bei `asyncio` auch nicht um Threads drum herum kommt, sofern man nicht tatsächlich nur mit Sockets arbeitet.
Im Fall des OP ist es so und ich verweise mal auf: https://github.com/FreeOpcUa/opcua-asyncio

Zur Zykluszeit: Falls der OPC UA Server auf der Steuerung betrieben wird, dann ist dort die Zykluszeit limitiert. Bei den Siemens-Steuerungen sind es z.B. minimal 100 ms Zykluszeit für OPC UA. Bei den hochwertigen Steuerungen kann man 10ms Zykluszeit für OPC UA einstellen.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Antworten