Performanceprobleme mit POSIX-Threads
-
Nun, die Arbeit der Worker ist im Prinzip eine Schreib-/Leseoperation auf dem Dateisystem, mit prinzipiell beliebiger Dateigröße und Datenmenge. Die Arbeit des Mainthreads und des Antwortthread ist es, mit dem Client über System Message Queues zu kommunizieren. Die Laufzeit des Main/Antwortthreads sollte daher <<< Workerthread sein.
Wenn das wirklich so ist, wie du es sagst, erstaunt mich das schon sehr, dass bloßes locken/unlocken so viel CPU Zeit frisst. Ist denn hier das Design falsch/schlecht, oder ist es schlicht so dass das locken/unlocken zu lange braucht?
Das Design des Programms lässt ein Packen der Aufgaben nicht zu; diese müssen immer einzeln abgearbeitet werden.
-
Es scheint weniger am "Aufwand" des lock und unlock zu liegen.
Eher, dass deine Threads aufeinander warten. (wie mcr beschrieben hat)Hast du für jeden Worker auch einen eigenen Mutex? Wäre sinnvoll, da sie ja unterschiedliche Listen haben.
Das Antwortthread.AddItem aus einem Worker scheint ein Flaschenhals zu sein. Eine Liste, viele wollen reinschreiben.
Du scheinst nun den Aufwand vom Process zum Antworten verschoben zu haben; das Process geht wahrscheinlich durch die vielen Threads schneller, aber das "Antworten" (Antwortthread) ist nun langsamer.
-
ihoernchen schrieb:
Es scheint weniger am "Aufwand" des lock und unlock zu liegen.
Eher, dass deine Threads aufeinander warten. (wie mcr beschrieben hat)Hast du für jeden Worker auch einen eigenen Mutex? Wäre sinnvoll, da sie ja unterschiedliche Listen haben.
Das Antwortthread.AddItem aus einem Worker scheint ein Flaschenhals zu sein. Eine Liste, viele wollen reinschreiben.
Du scheinst nun den Aufwand vom Process zum Antworten verschoben zu haben; das Process geht wahrscheinlich durch die vielen Threads schneller, aber das "Antworten" (Antwortthread) ist nun langsamer.
Jeder Worker hat ein eigenes Mutex, da jeder Worker eine eigene Instanz der Klasse ist. Die gemessenen Performanceunterschiede waren mit nur 1 Worker.
Eine Alternative mit mehreren Antwort-Threads habe ich auch schon in Betracht gezogen. Da die niedrige CPU Auslastung und schlechte Performance allerdings schon mit nur einem Worker auftritt, wollte ich erst schauen ob nicht ein generelles Problem vorliegt. Eine Lösung mit einem Antwort-Thread pro Worker wäre denkbar.
Zu Testzwecken habe ich auch bereits probiert, den Worker direkt processItem() vom Antwortthread aufrufen zu lassen, so dass das Listenabarbeiten nur im Worker stattfindet (das Programm hat dann also 2 Threads). Resultiert in der gleichen schlechten Performance.Folgende Annahmen für Tests mit einem Worker:
Sonderlich viel Warten im Bezug auf das Abarbeiten sollte in meinen Augen nicht auftreten. Der Worker kann jederzeit sein Item dem Antwortthread in die Liste schreiben, Ausnahme ist allein, wenn der Antwortthread gerade selbst die Liste auf Items prüft (alle 10 Sekunden oder nachdem ein Item abgearbeitet wurde). Gleiches gilt für den Mainthread.
Da die Anfrage blockiert, d.h. keine neue Anfrage kommt bis die aktuelle beantwortet wurde, sollte auch die Regel sein, dass der Antwortthread sofort wieder im sleep steckt, bis die nächste Anfrage kommt. Was ich mir vorstellen könnte, dass das mutex lock/unlock dann sleep/broadcast Zeit kostet. Wieviel so ein lock oder broadcast kostet, weiß ich nicht wirklich. Es wird allerdings tatsächlich einpthread_cond_broadcast(&cond);
verwendet. Vielleicht wäre es besser,
pthread_cond_signal(&cond);
zu verwenden, da ich sowieso zu jeder Zeit nur einen Thread habe, der auf diese Condition Variable wartet. Hab allerdings keine Ahnung, ob das performancetechnisch einen Unterschied macht.
Hatte in diesem Zusammenhang auch vergessen zu erwähnen, dass daswaitCond.timedWaitMs(mutex, 10000);
ein Wrapper für
pthread_cond_timedwait(&cond, &mutex, &abstime)
ist.
-
Du bist noch nicht genug auf mcrs Einwand eingegangen. Wenn die Jobs zu klein sind, lohnt sich der Aufwand für die Contextswitches und Synchronisation nicht.
Ansonsten zeig mal mehr Code.
-
Ponto schrieb:
Du bist noch nicht genug auf mcrs Einwand eingegangen. Wenn die Jobs zu klein sind, lohnt sich der Aufwand für die Contextswitches und Synchronisation nicht.
Ansonsten zeig mal mehr Code.
Mehr Code kann (darf) ich nicht zeigen. Aber wie schon angesprochen, jeder Worker führt Dateioperationen durch. Das heißt, lesen/schreiben/seek etc. auf eine Datei, wobei durchschnittlich etwa 200-300 Byte in die Datei geschrieben bzw. von ihr gelesen werden.
Also bietet sich als Optimierung nur das Zusammenfassen mehrer Aufgaben an? Also einen Worker mit vielen Dateien beschäftigen? Dann hätte ich aber vermutlich immer noch das Problem, dass der Worker für jeden Vorgang (obwohl er für z.B. 10 Dateien Arbeits-Items in der Liste hat) einmal die Liste locken und unlocken muss, um das nächste abzuarbeiten
-
Hat jemand vielleicht eine Idee, wie man die vergebene Zeit der Locks/Unlocks messen kann?
-
Also ich bin nun einen Schritt weiter. Das Problem aktuell scheint nicht die zu geringe Arbeit zu sein. Vielmehr blockieren sich die Threads zu oft gegenseitig.
Ich habe dafür folgenden Test durchgeführt:
- 2 bis n Worker
- ein Client pro Worker, d.h. jeder Worker arbeitet zu jeder Zeit nur Anfragen für genau einen Client abWeiterhin kann jeder Client erst dann eine neue Anfrage abschicken, wenn die aktuelle Anfrage beantwortet wurde. Das heißt, rein theoretisch dürfte bei einem beliebigen Worker erst dann wieder eine Anfrage eintreffen, wenn die letzte erfolgreich vom Worker verarbeitet wurde, an den Antwortthread weitergeleitet und dann an den Client zurückgeschickt wurde.
Das heißt weiterhin, dass jeder Worker zu jeder Zeit nur eine Anfrage abarbeitet, und in der Liste somit maximal ein Eintrag steht.Ich hab die addItem Methode nun ein bisschen erweitert. Statt mutex.lock() wird mutex.trylock() verwendet. So kann ich sehen, wann und wie lange auf ein gesperrtes Mutex gewartet wird.
int i= 0; while (mutex.tryLock() == fail) i++; if (i) printf("%s: locked mutex after (%d) tries\n", threadName(), i); list.append(item); ...
Dieser Zähler gilt für Worker und Antwortthread (da gleiche Basisklasse).
Wenn ich jetzt einen test mit 2 Clients starte, die viele Anfragen hintereinander abschicken, bekomm ich folgende Ausgabe:
Responder: locked mutex after (4) tries Responder: locked mutex after (1) tries Responder: locked mutex after (35) tries Worker 1 : locked mutex after (616) tries Responder: locked mutex after (14) tries Responder: locked mutex after (12) tries Responder: locked mutex after (12) tries Responder: locked mutex after (1) tries Responder: locked mutex after (12) tries Responder: locked mutex after (8) tries Worker 2 locked mutex after (54) tries Responder: locked mutex after (22) tries Responder: locked mutex after (13) tries Worker 1 locked mutex after (594) tries
Dass beim Responder uU auf ein gesperrtes Mutex gewartet wird, ist in diesem Fall in Ordnung, und allein durch das Design gegeben. Aber auf knapp 700 Zeilen der Ausgabe kommen 120 Zeilen in denen ein Worker auf ein gesperrtes Mutex trifft. Wenn man sich den Code der runloop anschaut, kann das nur vorkommen, wenn der Thread a) nicht in processItem() steckt oder b) nicht im sleep hängt.
Ich erklär mir das im Moment so, dass eine Anfrage die vom Worker abgearbeitet und durch den Responder beantwortet ist, schneller wieder im Client ist, dieser dann eine neue Anfrage schickt, die d er Hauptthread dem Worker in die Liste hängt, als der Worker nach processItem() wieder im sleep hängt.
Interessanterweise bekomm ich bei nur einem Client der viele Anfragen schickt keine Ausgabe. D.h. weder im Worker noch im Responder trifft er auf ein gesperrtes Mutex. Ich denke mal dass hier das Scheduling mit reinspielt.
Hat jemand vielleicht eine Erklärung, oder eine Lösung?
-
Mit tryLock() etwas zu messen ist sinnlos, da man anderen Threads damit keine Gelegenheit gibt, ihre Arbeit schnell zu beenden. Besser ist es die Zeit zwischen Aufruf des Locks und Erhalten des Locks zu messen.
Ohne Code kann man dir nicht helfen. Ich bin mir sicher, dass du ein Beispiel hinbekommst, das dein Problem zeigt, ohne euren sensitiven Code zu offenbaren. Bis dahin wirst du wohl vergeblich warten.
-
Parallelisierung bringt ja bei rein-rechenintensiven Programmen nur was, wenn das System mehrere Prozessoren hat. Oder wenn kurzfristig auf eine langsame Resource zugegriffen wird, so dass diese durch Parallelisierung die ganze Zeit ausgenutzt wird.
Ist beides nicht der Fall gibt es einfach nur Mehraufwand durch die Parallelisierung und Synchronisation.Ich hatte mal etwas ähnliches gemacht, allerdings gabs im Mainthread eine Queue (keine Liste!) aus der sich ein jeweils freier Workerthread das nächste Element nimmt und dann das Ergebnis ebenfalls in eine Queue an einen anderen Thread weitergibt.
Mich würde bei deiner Lösung mal interessieren, wie du die Aufgaben auf die Threads im vornherrein verteilst. Und ob die "Aufgaben" der Worker immer relativ ähnlich lange dauern, oder ob es stark variiert.
Bei mir gabs nämlich kaum einen Unterschied (unter 10%):
Im Test habe ich 10000 strings ("test"+laufende nummer) in eine queue gesteckt [main], dann aus der queue gelesen, den string in eine datei mit dem selben namen geschrieben und an eine andere queue wieder angefügt [worker]. Zuletzt wieder aus der queue gelesen und alle elemente ausgegeben [answer].Mit mehr Worker-Threads wirds besser, aber irgendwann wirds auch wieder schlechter. Alles auf einem Rechner mit einem Prozessor/Core.
EDIT:
Zur Zeitmessung habe ich folgende Klasse verwendet:class timer { struct timespec ts_start, ts_end; public: void start() { clock_gettime(CLOCK_MONOTONIC, &ts_start); } void stop() { clock_gettime(CLOCK_MONOTONIC, &ts_end); } timer(bool printres = false) { if (printres) { struct timespec ts_res; clock_getres(CLOCK_MONOTONIC, &ts_res); printf("Resolution: %lu s and %li ns\n", ts_res.tv_sec, ts_res.tv_nsec); } start(); } friend std::ostream & operator<<(std::ostream& os, const timer &t); }; std::ostream & operator<<(std::ostream& os, const timer &t) { unsigned long ns = (t.ts_end.tv_sec - t.ts_start.tv_sec) * 1000000000 + t.ts_end.tv_nsec - t.ts_start.tv_nsec; if (ns >= 10000) { ns /= 1000; if (ns >= 10000) os << ns / 1000 << " ms"; else os << ns << " us"; } else os << ns << " ns"; return os; }
Benutzung dann mit:
timer t; //timer t(true); würde noch die Auflösung des Timers ausgeben. //optional: t.start() //wird schon im Konstruktor aufgerufen, //aber für wiederverwendung etc //Tu was, was gemessen werden soll t.stop(); cout << "Etwas hat " << t << " gedauert!" << endl;
EDIT2:
Für die Simulation der langsamen Festplatte, wurden die Dateien mitO_SYNC
geöffnet. So ist nach dem Schreiben der Datei im Programm die Datei auch wirklich auf der langsamen Festplatte. Sonst wäre sie im Cache/RAM und der ist nun nicht der langsamste.
Wobei der Worker halt auch etwas "rechnen" muss, da sonst die Festplatte auch im seriellen Programm nahezu ganz ausgenutzt wird!
-
Getestet wurde auf Systemen mit 2 Pentium4 bzw. 4 Xeon CPUs. Alle Worker-Threads haben dabei exakt die gleiche Aufgabe, d.h. den gleichen Arbeitsaufwand.
@Ponto:
Mit trylock sollte nichts gemesssen werden. Es zeigt vielmehr, dass der Hauptthread im Worker auf ein gesperrtes Mutex trifft, obwohl das nicht der Fall sein sollte. Das ist die Sache, die ich mir nicht erklären kann, und die unter Umständen bei entsprechend vielen Worker-Threads auch die Performance kostet. Top zeit mir unter Last um die 50% in sys an, d.h. hier liegt wohl das Problem.