Ich habe eine USB High Speed Board/projekt und will über USB ca 400kB/s
streamen und es via python aufnehmen.
Ich kriege öfter missing packete. Genaugenommen (siehe code unten)
A: mit time.sleep(0.1) klappt es
B: mit time.sleep(0.2) Bekomme ich USBD_BUSY in ST USB Library.
Auf der ST Seite habe ich leider keine memory um ein resend zu machen.
set_buffer_size() hilft auch nicht. Hat jemand eine Idee was ich sonst
machen konnte?
import serial
import serial.tools.list_ports
....
mySerial = serial.Serial(port=port, baudrate=2000000, timeout= 1)
mySerial.set_buffer_size(rx_size=500000, tx_size=500000)
While(1):
bytes_available = mySerial.in_waiting
a = mySerial.read(bytes_available)
time.sleep(0.2)
Mat. K. schrieb:
> While(1):
> bytes_available = mySerial.in_waiting
> a = mySerial.read(bytes_available)
> time.sleep(0.2)
Das ist, mit Verlaub gesagt, Unsinn.
›While‹ klein, Klammern weg, Bool Wert und vieles andere mehr… 1 | #!/usr/bin/python3
| 2 | # -*- coding: UTF-8 -*-
| 3 | # vim: fileencoding=utf-8: ts=4: sw=4: expandtab:
| 4 | import serial
| 5 | import time
| 6 |
| 7 | mySerial = serial.Serial(port='/dev/ttyACM0', baudrate=2000000, timeout=1)
| 8 | while True:
| 9 | while (bytes_available := mySerial.in_waiting) <= 0:
| 10 | time.sleep(1/1000)
| 11 | data = mySerial.read(bytes_available)
| 12 | print(len(data))
|
Tatsächlich löst man diese einfache Aufgabe jedoch mittels select/poll.
Norbert schrieb:
> Tatsächlich löst man diese einfache Aufgabe jedoch mittels select/poll.
Ist das überhaupt nötig? Die read Funktion sollte doch einfach
blockieren wenn keine Bytes verfügbar sind.
Allerdings sind die CDC-Treiber der Betriebssysteme nicht wirklich für
solche Geschwindigkeiten ausgelegt.
Niklas G. schrieb:
> Ist das überhaupt nötig? Die read Funktion sollte doch einfach
> blockieren wenn keine Bytes verfügbar sind.
Er hat einen Timeout angegeben.
Niklas G. schrieb:
> Allerdings sind die CDC-Treiber der Betriebssysteme nicht wirklich für
> solche Geschwindigkeiten ausgelegt.
Die CDC Treiber meines Betriebssystems schafft da noch deutlich mehr.
DEUTLICH!
Norbert schrieb:
> Er hat einen Timeout angegeben.
Dann ruft man "read" einfach nochmal auf bei Timeout.
Norbert schrieb:
> Die CDC Treiber meines Betriebssystems schafft da noch deutlich mehr.
> DEUTLICH!
Welches ist das?
Hi
ich teste es gerade auf Windows. Linux/mac folgen nocht.
Das problem ist das time.sleep(0.2 oder mehr)
Ohne den Sleep habe ich keine Probleme.
Dieser "sleep" ist place holder für ein dutzend Threads die auf dem PC
noch laufen.
Niklas G. schrieb:
> Welches ist das?
Das ist ein Debian GNU/Linux stable.
Von einem Discovery STM32F4 zum PC stellen sich geschmeidige 1MB/s ein.
(Großes B)
Noch eines zum select/poll Ansatz:
Da ist die Nutzung vergleichbar mit dem Essen einer Suppe. Wenn man
einmal von Gabel auf Löffel umgestiegen ist, möchte man nicht mehr
zurück.
Mat. K. schrieb:
> Dieser "sleep" ist place holder für ein dutzend Threads die auf dem PC
> noch laufen.
Aha. Ein Scheibchen Salami!
Hatte ich in diesem Zusammenhang schon select/poll und jetzt noch
zusätzlich asyncio angesprochen?
Die anderen Threads interessieren erstmal nicht. Ich will in der Lage
sein trotz time.sleep(0.2) mein stream hin zu bekommen. Verhält sich da
pyusb besser?
wenn ich es richtig sehe
funktioniert das hier nicht:
mySerial.set_buffer_size(rx_size=500000, tx_size=500000)
Mat. K. schrieb:
> Die anderen Threads interessieren erstmal nicht
Die interessieren sehr wohl. Denn Python kann überhaupt kein echtes
Multithreading sondern führt immer nur einen Thread aus (GIL). Wenn
jetzt ein Thread in "read" blockiert kann kein anderer laufen.
Prinzipiell sollte das bei IO-Funktionen "eigentlich" nicht passieren,
aber wer weiß. Wenn ein anderer Thread den Python-Interpreter blockiert
könnte das "read" gar nicht erst rechtzeitig aufgerufen werden.
Mat. K. schrieb:
> Verhält sich da
> pyusb besser?
Das ist grundsätzlich der bessere Ansatz, weil du da aus Python die
Paketübertragung direkt steuern kannst. Heißt insbesondere, das Gerät
kann gar keine Daten senden, bevor du den Transfer per pyusb
angefordert hast, d.h. es können zwischen Gerät und Python-Anwendung
keine Daten verloren gehen (anders als bei CDC). Allerdings musst du
natürlich sicherstellen, dass die Datenrate immer hoch genug ist, damit
du nicht auf dem Gerät einen Pufferüberlauf bekommst.
Mat. K. schrieb:
> Ich will in der Lage
> sein trotz time.sleep(0.2) mein stream hin zu bekommen.
Tja, mit dem ›Wollen‹ ist das immer so eine Sache.
Man kann sich zu Weihnachten für einen Monat ins Bett legen wollen, darf
sich dann aber nicht beschweren wenn man Silvester verpasst.
> Verhält sich da pyusb besser?
›Serial‹ verhält sich tadellos, wenn man es denn korrekt anwendet.
Lösungsvorschläge hast du, aber du machst das schon irgendwie…
Niklas G. schrieb:
> Heißt insbesondere, das Gerät
> kann gar keine Daten senden, bevor du den Transfer per pyusb
> angefordert hast, d.h. es können zwischen Gerät und Python-Anwendung
> keine Daten verloren gehen (anders als bei CDC).
Er will kontinuierlich streamen und er hat auf dem Gerät keinen Platz
für einen Puffer. Das macht's dann schon schwierig, wenn man kategorisch
auf unsachgemäße Programmierung besteht.
Niklas G. schrieb:
> Die interessieren sehr wohl. Denn Python kann überhaupt kein echtes
> Multithreading sondern führt immer nur einen Thread aus (GIL). Wenn
> jetzt ein Thread in "read" blockiert kann kein anderer laufen.
> Prinzipiell sollte das bei IO-Funktionen "eigentlich" nicht passieren,
> aber wer weiß. Wenn ein anderer Thread den Python-Interpreter blockiert
> könnte das "read" gar nicht erst rechtzeitig aufgerufen werden.
Was seit python 3.13.1 nicht mehr ganz stimmt. Man kann während der
Installation auch den Haken machen bei "download free-threaded binaries"
und ist den blockierenden GIL-Lock los.
Jürgen schrieb:
> Was seit python 3.13.1 nicht mehr ganz stimmt. Man kann während der
> Installation auch den Haken machen bei "download free-threaded binaries"
> und ist den blockierenden GIL-Lock los.
Sollte man das nicht mögen, sondern mit dem Standard Python arbeiten
wollen, so gibt es im Manual:
* im Allgemeinen die Sektion ›Concurrent Execution‹ und
* im Speziellen die Sektion ›subprocess‹
mitsamt passender Prozess Kommunikation.
zu GIL & multi threading im allgemeinen :
es gibt I/O-bound tasks und CPU-bound tasks. In case of USB CDC reden
wir von I/O bound. Damit kommt python sehr gut zu recht: heisst der
serial read is nicht blocking.
Darum geht es nicht.
Norbert schrieb:
> Sollte man das nicht mögen
Man sollte sich aber langsam damit befassen, dass Python endlich
erwachsen wird. Eine Programmiersprache die kein richtiges
Multithreading beherrscht verdient es nicht als solche bezeichnet zu
werden...
Nodejs ist auch so eine Krücke aber da ist das fehlende richtige
Multithreading nur eines der Übel.
Jürgen schrieb:
> Man sollte sich aber langsam damit befassen, dass Python endlich
> erwachsen wird.
Aaaaaaaaa…haaaaaaaaaaa.
Danke für die erleuchtende Information.
…und für diejenigen welche sich bis jetzt noch nicht Kopfschüttelnd
abgewandt haben…
So sieht es aus wenn ein Standard Python Programm vier Prozesse auf vier
Kernen der CPU ausführt. In … Moment … 19 Zeilen Code. 1 | running on core:0
| 2 | running on core:2
| 3 | running on core:1
| 4 | running on core:3
| 5 | ended core:0
| 6 | ended core:3
| 7 | ended core:1
| 8 | ended core:2
|
Norbert schrieb:
> …und für diejenigen welche sich bis jetzt noch nicht Kopfschüttelnd
> abgewandt haben…
> So sieht es aus wenn ein Standard Python Programm vier Prozesse auf vier
> Kernen der CPU ausführt. In … Moment … 19 Zeilen Code.
Danke auch für die überaus erleuchtende Information die komplett
nichssagend ist.
Wenn es keinen Handlungsbedarf an dieser Stelle gibt, müssen die Python
Entwickler ja komplett hirnbefreit sein wenn sie sich mit sowas
Unsinnigem beschäftigen.
Da gibt es auch noch andere Baustellen. Versuch mal Python in C++ oder
Rust multithreaded zu embedden. Also mit isoliertem GIL. Da wirst du
schnell feststellen, dass große Teile der Libs noch nicht damit umgehen
können. Ganz vorn dabei ctypes. Jedenfalls bis 3.12. In der 3.13 ist das
jetzt behoben. Damit passt es dann wieder zur Aussage mit dem erwachsen
werden.
Jürgen schrieb:
> Wenn es keinen Handlungsbedarf an dieser Stelle gibt, müssen die Python
> Entwickler ja komplett hirnbefreit sein wenn sie sich mit sowas
> Unsinnigem beschäftigen.
Ich nehme mal zu deinen Gunsten an, dass du dich nur dumm stellst
Selbstverständlich kann und sollte man Python weiter verbessern. Das
steht außer Frage.
Aber es ist schon jetzt völlig problemlos möglich in einem Python
Programm alle verfügbaren Kerne gleichzeitig und somit einen ganzen
Sack voller Prozesse zu nutzen.
Ganz im Gegensatz zu deiner kategorischen Aussage:
> Eine Programmiersprache die kein richtiges Multithreading beherrscht
> verdient es nicht als solche bezeichnet zu werden...
Norbert schrieb:
> und somit einen ganzen
> Sack voller Prozesse zu nutzen.
>
> Ganz im Gegensatz zu deiner kategorischen Aussage:
>> Eine Programmiersprache die kein richtiges Multithreading beherrscht
>> verdient es nicht als solche bezeichnet zu werden...
Multithreading != Multiprocessing
Norbert schrieb:
> Er will kontinuierlich streamen und er hat auf dem Gerät keinen Platz
> für einen Puffer.
Eine gewisse Menge Puffer hat man immer. Ein STM32 mit USB High-Speed
hat vermutlich genug SRAM um für mindestens 100ms zu puffern, außer das
ist alles schon belegt...
Ich würde sowas mit libUSB und nativem USB-Protokoll in C++ machen.
Obwohl sowas grundsätzlich auch mit asynchronem IO geht ist der Support
dafür eher stiefmütterlich, daher würd ich das libusb_bulk_transfer in
einer Schleife in einem dedizierten Thread aufrufen (C++ hat natürlich
echte Threads).
Niklas G. schrieb:
> Multithreading != Multiprocessing
Das ist richtig. Andererseits werden zum Beispiel auf Linux System gerne
LWPs genutzt. Und schon verschwimmen die Grenzen.
Im Übrigen ist - ebenfalls bei Linux - die Erstellung eines Prozesses
erstaunlich Kostengünstig (CPU-Zeit).
Und wenn man nicht ständig Zettabytes an Daten zwischen den Prozessen
hin und her schiebt, dann gibt es kaum (im Sinne von fast keine)
Nachteile.
Aber um mal wieder den Bogen zu diesem Thread (no pun intended) zu
schlagen, alle ›scheinbaren‹ Probleme mit dem Datenempfang ließen sich
mit sehr wenigen Zeilen Code lösen.
Und ein einziges mal einen Prozess unter Windows zu generieren kann man
CPU-Zeit mäßig sicherlich verschmerzen.
Und man bräuchte sich danach noch nicht einmal um irgendwelche
sleep/delay Unsauberheiten zu kümmern. Ein paar hundert KB/s, auch ein
paar MB/s eingehend sind wirklich Kinderkram.
Norbert schrieb:
> Im Übrigen ist - ebenfalls bei Linux - die Erstellung eines Prozesses
> erstaunlich Kostengünstig (CPU-Zeit).
Auch wenn die Erstellung eines Prozesses vergleichsweise günstig
erfolgen kann, ist und bleibt es Schwachsinn, einen solchen zu erzeugen
ohne jede Not.
Und diese Not kann nur eine sein: Eine GEWÜNSCHTE Isolation der Daten
zwischen den Prozessen. Das genau ist aber hier gerade nicht gewünscht,
sondern im Gegenteil vollständige Teilung mit minimalem negativen
Performance-Impact.
Also: klare Aufgabe für Threads.
Ob S. schrieb:
> Auch wenn die Erstellung eines Prozesses vergleichsweise günstig
> erfolgen kann, ist und bleibt es Schwachsinn, einen solchen zu erzeugen
> ohne jede Not.
>
> Und diese Not kann nur eine sein: Eine GEWÜNSCHTE Isolation der Daten
> zwischen den Prozessen.
Danke. Genau so sieht es aus. Und wenn man auf gemeinsame Daten in
verschiedenen Threads angewiesen ist, dann ist es erst ab Python 3.13
überhaupt sinnvoll. So lange ist es auch noch nicht her dass es
überhaupt isolierte Interpreter in Python gibt. Etwas was PHP seit
vielen Jahren von Anfang an beherrscht hat. Auch nodejs mit der V8 hat
sich da lange schwer getan. Aber genug damit, der Threadstarter hatte
andere Probleme.
Ob S. schrieb:
> Also: klare Aufgabe für Threads.
Früher(tm) war halt fork das einzig wahre. Ein echtes Unix kannte
keine Threads.
Nun Gentlemen, da sind wir doch noch zu einem für alle
zufriedenstellenden Ergebnis gekommen. Wieder einmal zeigt sich, man
muss nur miteinander reden.
Halten wir fest: Die Einen können die gestellte Aufgabe mittels Python
lösen, die Anderen eben nicht. Dafür verfechten sie jedoch die reine
Lehre, was auch gut ist. Muss mal erwähnt werden.
Ich hab's gerade mal aus reinem Spaß an der Freude auf einem >20 Jahre
alten Pentium III(coppermine / single core) getestet.
Da dauert die (nur einmalig erforderliche) Erstellung eines Prozesses
tatsächlich unerträglich lang. Ich habe dennoch die vollen 22
Millisekunden abgewartet und danach lief das Programm prima.
Bedankt für die wertvolle Diskussion. ;)
Norbert schrieb:
> Da dauert die (nur einmalig erforderliche) Erstellung eines Prozesses
> tatsächlich unerträglich lang.
Und wie lange dauert das Rüberschaufeln eines Datenblocks vom
USB-Prozess in den Hauptprozess? Wie lange wird die Empfangsschleife
dabei blockiert?
Niklas G. schrieb:
> Und wie lange dauert das Rüberschaufeln eines Datenblocks vom
> USB-Prozess in den Hauptprozess? Wie lange wird die Empfangsschleife
> dabei blockiert?
Shared memory, rate doch mal…
Norbert schrieb:
> Shared memory, rate doch mal…
Dann rate ich 2.7 Sekunden, d.h. zu lange.
Niklas G. schrieb:
> Norbert schrieb:
>> Shared memory, rate doch mal…
>
> Dann rate ich 2.7 Sekunden, d.h. zu lange.
Nein, nein. Eine kurze ›Fertig1/Fertig2‹ Message rüber senden und direkt
auf den Speicher zugreifen, da braucht's eher 2.7 Tage. Und das ist noch
konservativ geschätzt.
Norbert schrieb:
> Message rüber senden
Wie sendest du die Messsage? Was ist das für ein
Synchronisationsprimitiv? Kann der Empfänger den Sender blockieren...?
Event. Ja (soll er aber ausdrücklich nicht)
Norbert schrieb:
> Event
Und wie lange dauert es damit? Welches Primitiv des OS wird da genutzt?
Wie groß ist die Latenz der diversen Kontextwechsel?
Niklas G. schrieb:
> Und wie lange dauert es damit?
Aus dem Kopf: Einige tausend pro Sekunde. Auf einem fünfzehn Jahre alten
dual core Intel Core i3-4010U.
Den Rest darfst du aber gerne selbst testen. Python ist kostenlos.
Dokumentation ist kostenlos. Rechner setze ich voraus. ;-)
Aber bitte denke immer daran, das ist alles verbotenes Teufelszeug!
Norbert schrieb:
> Aber bitte denke immer daran, das ist alles verbotenes Teufelszeug!
Ich hab doch nur gefragt...
Norbert schrieb:
> Jürgen schrieb:
>> Was seit python 3.13.1 nicht mehr ganz stimmt. Man kann während der
>> Installation auch den Haken machen bei "download free-threaded binaries"
>> und ist den blockierenden GIL-Lock los.
>
> Sollte man das nicht mögen,
... zum Beispiel, weil das Feature noch "experimental" ist ...
> sondern mit dem Standard Python arbeiten
> wollen, so gibt es im Manual:
> * im Allgemeinen die Sektion ›Concurrent Execution‹ und
> * im Speziellen die Sektion ›subprocess‹
>
> mitsamt passender Prozess Kommunikation.
Daß das Modul "multiprocessing" eine sehr ähnliche API wie "threading"
hat, sollte an dieser Stelle lobend erwähnt werden. Diese Ähnlichkeit
macht es einfach, von Threading auf Multiprocessing zu wechseln. :-)
Ob S. schrieb:
> Auch wenn die Erstellung eines Prozesses vergleichsweise günstig
> erfolgen kann, ist und bleibt es Schwachsinn, einen solchen zu erzeugen
> ohne jede Not.
>
> Und diese Not kann nur eine sein: Eine GEWÜNSCHTE Isolation der Daten
> zwischen den Prozessen. Das genau ist aber hier gerade nicht gewünscht,
> sondern im Gegenteil vollständige Teilung mit minimalem negativen
> Performance-Impact.
> Also: klare Aufgabe für Threads.
So eine Auswahl treffen nur Laien anhand ihrer WÜNSCHE. Profis
orientieren sich stattdessen vielmehr daran, was NÖTIG ist.
Wer einen "minimalen negativen Performance-Impact" braucht, entwickelt
doch nicht in einer interpretierten Skriptsprache. Der benutzt einen
optimierten Interpreter wie Pypy oder IronPython. Der nutzt
Bibliotheken, die, wie numpy und etliche andere, ohnehin nicht vom GIL
betroffen sind. Oder er entwickelt die performancekritischen Teile
seiner Software in einer anderen Sprache mit Python.h (C), Boost::Python
(C++), PyO3 (Rust) oder Cgo (Golang). Oooder er verwendet einen der
verfügbaren Python-Compiler wie nuitka oder numba. Alle diese
Möglichkeiten existieren, obwohl ein "minimaler negativer
Performance-Impact" für die allermeisten Programme ohnehin gar nicht
notwendig ist.
Niklas G. schrieb:
> Und wie lange dauert es damit? Welches Primitiv des OS wird da genutzt?
> Wie groß ist die Latenz der diversen Kontextwechsel?
Hab' jetzt mal mit der wirklich schlechtest möglichen 0815 plain Jane
Herangehensweise und völlig ohne Optimierungen getestet.
Da serielle Schnittstellen zum Testen selbstverständlich viel zu
lahmarschig sind, sendet ein Prozess statt dessen mit maximaler
Geschwindigkeit 10000 Blöcke (bytearrays) (jeweils mit unterschiedlicher
Größe.)
Die werden bei der Übergabe an den Empfänger-Prozess heftigst im RAM
herum kopiert. Der zweite Prozess nimmt die Daten jeweils komplett an
und verwirft sie dann.
Auf einem alten Desktop Rechner: 1 | inxi -C
| 2 | CPU: Info: Quad Core model: Intel Core i5-3470 bits: 64 type: MCP L2 cache: 6 MiB
| 3 | Speed: 1689 MHz min/max: 1600/3600 MHz
|
ergeben sich je nach Blockgröße: 1 | 2^14: 744.6 MB/s
| 2 | 2^15: 1193.0 MB/s
| 3 | 2^16: 1267.7 MB/s
| 4 | 2^17: 1724.1 MB/s
| 5 | 2^18: 885.4 MB/s
|
Norbert schrieb:
> ergeben sich je nach Blockgröße:
Der Durchsatz ist schonmal gut, aber viel wichtiger ist hier die Latenz,
genauer die Dauer der Sende-Operation. Sonst wird der Serial/USB-Thread
zu lange blockiert.
Niklas G. schrieb:
> Norbert schrieb:
>> ergeben sich je nach Blockgröße:
>
> Der Durchsatz ist schonmal gut, aber viel wichtiger ist hier die Latenz,
> genauer die Dauer der Sende-Operation. Sonst wird der Serial/USB-Thread
> zu lange blockiert.
Ähm, nein, wird er nicht. Erstens und zweitens hat der OS-Treiber einen
eigenen Buffer und läuft völlig unabhängig. Man hat also Zeit die Daten
abzuholen. Hier reden wir von einigen 100µs.
Nee … Moment … ich mess' mal … fast immer deutlich weniger: 1 | average: 14.4 µs
| 2 | Transfer (2^10): 57.5 MB/s
| 3 | average: 15.2 µs
| 4 | Transfer (2^11): 109.9 MB/s
| 5 | average: 16.1 µs
| 6 | Transfer (2^12): 210.1 MB/s
| 7 | average: 18.0 µs
| 8 | Transfer (2^13): 382.3 MB/s
| 9 | average: 22.6 µs
| 10 | Transfer (2^14): 621.7 MB/s
| 11 | average: 29.1 µs
| 12 | Transfer (2^15): 987.2 MB/s
| 13 | average: 53.9 µs
| 14 | Transfer (2^16): 1113.2 MB/s
| 15 | average: 100.6 µs
| 16 | Transfer (2^17): 1230.1 MB/s
| 17 | average: 343.7 µs
| 18 | Transfer (2^18): 747.6 MB/s
|
Transferraten etwas geringer, da tausende von Messdaten während der
Übertragung gespeichert werden müssen.
Niklas G. schrieb:
> Der Durchsatz ist schonmal gut,
Das ist vermutlich der schlechtest Mögliche. Mit weiteren zehn bis
zwanzig Zeilen Code würde ich eine Erhöhung um eine Größenordnung
erwarten.
Norbert schrieb:
> Ähm, nein, wird er nicht.
Wie funktioniert das dann? Wie kann man Daten abschicken während
gleichzeitig eine "read" Operation läuft?
Norbert schrieb:
> und zweitens hat der OS-Treiber einen
> eigenen Buffer
Bei CDC ja, bei direktem USB-Zugriff (z.B. pyusb) nicht. Gerade unter
Windows funktioniert dieser Puffer nicht so toll und verschluckt gerne
mal Daten.
Norbert schrieb:
> Hier reden wir von einigen 100µs.
Das passt, dann ist die Frage geklärt.
Bei USB LS und FS muss man noch nicht einmal darüber nachdenken.
Und selbst bei USB HS HID werden kaum mehr als 8MB/s** anfallen.
** 1000ms/s × 8 microframes/ms × 1024 Bytes/microframe
(Falls ich das recht erinnere)
Norbert schrieb:
> Bei USB LS und FS muss man noch nicht einmal darüber nachdenken.
Mit genug Overhead und schlechter Programmierung kann man selbst
langsame Übertragungen durcheinander bringen. Das ist ja selbst bei
Zugriffen innerhalb des Computers so, wenn man z.B. einzelne Bytes in
den RAM oder die Konsole schreibt, obwohl es Caches/Puffer gibt.
Niklas G. schrieb:
> Mit genug Overhead und schlechter Programmierung kann man selbst
> langsame Übertragungen durcheinander bringen.
Da hilft dann nur noch striktes Tastaturverbot und zurück schicken zum
Teig kneten…
Norbert schrieb:
> und zurück schicken zum
> Teig kneten…
Da kann man auch genug falsch machen!
Harald K. schrieb:
> Ob S. schrieb:
>> Also: klare Aufgabe für Threads.
>
> Früher(tm) war halt fork das einzig wahre. Ein echtes Unix kannte
> keine Threads.
Früher(tm) war fork das einzig verfügbare. Hätte es Threads gegeben,
dann hätte man sie auch genutzt.
Das ist aber eh alles schon lange her. Der pthreads-Standard wurde vor
30 Jahren verabschiedet, ein Jahr später bekam Linux eine erste Variante
von Thread-Support. Dass Python das bis heute nicht sauber hinbekommen
hat, ist da nicht gerade glorreich.
Rolf M. schrieb:
> fork
Verrückt, oder? Gibt's seit gut fünfzig Jahren, ist FOSS, darf also
jeder benutzen, wird viele hundert Millionen Mal am Tag genutzt und es
funktioniert immer noch für eine sehr große Anzahl an Aufgaben absolut
tadellos.
Das nenne ich mal Nachhaltig. ;-)
Norbert schrieb:
> Das nenne ich mal Nachhaltig. ;-)
Naja, es wurde schon einiges dran rumoptimiert damit es weiterhin gut
funktioniert, insbesondere das mit dem Copy-On-Write der Pages. Aber ja,
es ist schon ein durchdachtes API.
Unter Windows könnte man es mit WinUSB und Overlapped IO besonders
effizient asynchron hinbekommen, unter Linux vermutlich mit epoll oder
so. Braucht dann nur 1 Thread/Prozess für alles I/O. Ist hier aber wohl
eher unnötig.
Norbert schrieb:
> Mit weiteren zehn bis
> zwanzig Zeilen Code würde ich eine Erhöhung um eine Größenordnung
> erwarten.
Da habe ich ja verdammt gut geraten. 1 | Transfer (2^10): 178.0 MB/s
| 2 | Transfer (2^11): 350.3 MB/s
| 3 | Transfer (2^12): 653.5 MB/s
| 4 | Transfer (2^13): 1288.3 MB/s
| 5 | Transfer (2^14): 2691.5 MB/s
| 6 | Transfer (2^15): 4900.8 MB/s
| 7 | Transfer (2^16): 7501.1 MB/s
| 8 | Transfer (2^17): 9693.2 MB/s
| 9 | Transfer (2^18): 11676.4 MB/s
| 10 | Transfer (2^19): 13308.0 MB/s
| 11 | Transfer (2^20): 12880.8 MB/s
|
Selbstverständlich mit Synchronisation/Signalisierung zwischen den
Prozessen.
Proc1 schreibt, Proc2 wartet bis fertig.
Proc2 liest, Proc1 wartet bis fertig.
Jetzt ist aber genug, 13GB/s sollten für die Daten einer USB
Kommunikation mehr als ausreichend sein. ;-)
Waren übrigens weniger als zehn Zeilen, da hatte ich schlecht geraten.
Rolf M. schrieb:
> Das ist aber eh alles schon lange her. Der pthreads-Standard wurde vor
> 30 Jahren verabschiedet, ein Jahr später bekam Linux eine erste Variante
> von Thread-Support. Dass Python das bis heute nicht sauber hinbekommen
> hat, ist da nicht gerade glorreich.
Nunja, die Referenzimplementierung von CPython war nie und ist immer
noch nicht performanceoptimiert. Wer seinen Python-Code performanter
haben will, verwendet einfach einen anderen Interpreter, zum Beispiel
Pypy mit seinem Just-in-Time-Compiler. Das GIL dient einer einfachen
Synchronisierung beim Zugriff mehrerer Threads auf dieselben Daten. Aber
wo mehrere Prozesse oder Threads auf dieselben Speicherobjekte zugreifen
sollen, sind doch derartige Synchronisierungen ohnehin zwingend
notwendig.
Außerdem gibt es schließlich Alternativen zum Multithreading, namentlich
das Modul "multiprocessing" anstelle des "treading"-Moduls. Das
"multiprocessing"-Modul hat nicht nur eine ähnliche API, die es zur
Ersetzung von "threading" empfehlen, sondern bietet auch noch viele
weiterer sinnvoller Features zur Interprozesskommunikation,
Synchronisierung, Pooling, und so weiter.
Nur so rein spaßeshalber habe ich mir mal ein paar kleine
Testprogrämmchen geschrieben, die Ganzzahlen auf der Basis eines
Startwertes 100 Millionen Mal inkrementieren und aus der Summe jeweils
die Wurzel ziehen. Ein Programm nutzt das Modul "threading", eines
"multiprocessing" und das letzte macht dasselbe mit Numpy, und die
Programme wurden je einmal mit Python in Version 3.12.3 und mit Pypy
Version 3.9.18 auf einem Intel Core i7-7820HQ ausgeführt. (Nein, das ist
kein richtiger Benchmark, nur eine schnelle Spielerei.)
Die Ergebnisse sind fast genau so, wie ich es erwartet hatte: mit
CPython und "threading", also dem GIL, benötigt das Programm ca. 196
Sekunden, mit Pypy und demselben Modul nur noch 9,5 Sekunden. Mit
"multiprocessing" und CPython benötigt das Programm hingegen nurmehr
55,2 Sekunden, wohingegen dasselbe Programm mit Pypy nur 2,0 Sekunden
benötigt.
Die Verwendung des "multiprocessing"- anstelle des "threading"-Moduls
sorgt daher für eine Beschleunigung von "nur" Faktor 3,5 -- nicht sehr
viel, aber immerhin doch recht deutlich. Viel mehr bringt die Verwendung
eines anderen Interpreters, nämlich im Fall von "threading" um den
Faktor 20 und im Falle von "multiprocessing" sogar um Faktor 27. Wer
sowohl "multiprocessing" als auch Pypy benutzt, beschleunigt die ganze
Veranstaltung somit um den Faktor 98(!) -- und das mit minimalen
Änderungen am Code.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
|