Multithread-Anwendung nutzt nur einen Kern
-
Hast Du das Header-Chaos mittlerweile mal bereinigt?
Schon mal über eine Interfaceklasse und einen Container für die Verwaltung nachgedacht?
Du musst nicht PostMessage verwenden, wenn Du FreeOnTerminate = true setzt... Es ist unnötig, die Thread-Instanz 'von Hand' zu löschen. Welchen Sinn siehst Du darin? Verwende SendMessage() und lass den Thread sich anschließend selbst löschen.
Ok, die WndProc zu überschreiben geht auch problemlos. War mir glatt entfallen, das es die auch noch gibt...
Einen Zugriffschutz kann man nicht umschiffen. Entweder man benötigt einen, oder eben nicht. Aber ein 'Umschiffen' bedeutet mit absoluter Sicherheit ein undefiniertes Verhalten.
-
Header-Chaos habe ich beseitigt, alle Memberfunktionen schön in .cpp's verpackt.
Die Interfaceklasse, würdest du die vor die Threads setzen?
Du musst nicht PostMessage verwenden, wenn Du FreeOnTerminate = true setzt... Es ist unnötig, die Thread-Instanz 'von Hand' zu löschen. Welchen Sinn siehst Du darin? Verwende SendMessage() und lass den Thread sich anschließend selbst löschen
Muss ich denn nicht den Speicher für den Thread freigeben? Oder erledigt er das selber?
-
benediktibk schrieb:
Header-Chaos habe ich beseitigt, alle Memberfunktionen schön in .cpp's verpackt.
Das war auch dringend nötig.
benediktibk schrieb:
Die Interfaceklasse, würdest du die vor die Threads setzen?
Die Interface-Klasse soll die Verwaltung vereinfachen. Alle relevanten Daten gehören da hinein:
- Die Parameter, die dem Thread übergeben werden.
- Die Ergebnisse, die im Thread berechnet wurden.
- Ein eindeutiges Kriterium, mittels dem der Thread identifiziert werden kann (zB die ThreadId)Für die Parameter und die Ergebnisse würde ich jeweils ein eigene Klasse verwenden. Die Parameterklasse kannst Du dem Thread als Zeiger übergeben. Im Thread erstellst Du ein Ergebnisobjekt, Wenn der Thread mit der Berechung beendet hat, sendest Du mittels SendMessage() eine Nachricht, mit der Id und einem Zeiger auf das Ergebnisobjekt. Das Ergebnisobjekt kopierst Du in das Interface-Objekt. Anschließend kannst Du das Ergebnisobjekt im Thread löschen und den Thread sich beenden und somit selbst löschen lassen, wenn Du FreeOnTerminate = true setzt.
Ich würde zusätzlich noch ein Status-Flag mit in die Interfaceklasse nehmen (hier sollte 3 Stati reichen, zur Ausführung vorgesehen, wird ausgeführt und ist beendet).
Die Interface-Objekte packst Du in einer Container (zB std::vector oder TList). Dies würde ich in der Berechnen-Button-Funktion machen. Dort würde ich die Interface-Objekte erzeugen und in den Container legen. Threads solltest Du an dieser Stelle noch nicht erzeugen. Im Prinzip enthält das Interface-Objekt zu diesem Zeitpunkt nur das Parameter-Objekt und das Statusflag steht auf 'zur Ausführung vorgesehen'
Die Koordination und Erzeugung der Threads würde ich in eine Schedulerfunktion packen, die dafür verantwortlich ist, die Threads zu erzeugen.
Diese Schedulerfunktion wird einmalig am Ende der Berechnen-Button-Funktion ausgelöst und jedes Mal, nachdem Du das Ergebnis in das Interface-Objekt kopiert hast. Nicht vergessen, das StatusFlag auf 'ist beendet' zu setzen.
In der Schedulerfunktion iterierst Du einfach durch den Container und prüfst das Statusflag des Interface-Objektes. Bei 'zur Ausführung vorgesehen' erzeugst Du einen neuen Thread, mit dem Parameter-Objekt aus dem Interface-Objekt, schreibst die Thread-Id in das Interface-Objekt und änderst das Statusflag entsprechend. Während Du durch den Container iterierst kannst Du zum einen ermitteln, wie viele Threads zur Zeit laufen und wie viele Objekte noch zur Berechnung vorgesehen sind. Falls die maximale Anzahl der Threads noch nicht erreicht ist und noch Objekte zur Berechnung vorgesehen sind, rufst Du die Scheduler Funktion einfach noch mal auf (oder Du machst eine Schleife aus der Schedulerfunktion, ist eigentlich egal).benediktibk schrieb:
Muss ich denn nicht den Speicher für den Thread freigeben? Oder erledigt er das selber?
Das ist ja das schöne an FreeOnTerminate. Sobald die Execute()-Methode beendet ist, löscht der Thread sich selbst.
-
Also, die Angelegenheit mit der Interface-Klasse, so wie du sie beschrieben hast, war mir dann doch etwas zu hoch, da ich keine Ahnung von Vektoren und ähnlichem habe, wodurch ich mich zuerst wieder einiges an Wissen aneignen müsste.
Deswegen habe ich mich dazu entschieden, die Tupel zusammen mit einem boolschen Wert in eine Struktur zu packen, von welcher ich mir je nach Anzahl der Tupel ein passendes Array erstelle. In dem boolschen Wert speichere ich mir, ob ich dieses Tupel bereits einem Thread zugewissen habe. Vom Thread bekomme ich dann in der Message das Ergebnis und den Indizes für das Struktur-Array, wodurch ich alle benötigten Informationen habe:
-Ergebnis
-Tupel-GliederDeinen Vorschlag für eine Interface-Klasse werde ich aber im Hinterkopf behalten und bei Bedarf einsetzen. :p
Das sieht dann so aus (etwas gekürzt):
struct SAufgabe { CTupel tupel; bool zugeteilt; };
void __fastcall TForm1::Button1Click(TObject *Sender) { int faktor=0; short start[3]; int l=0; bool varianten[8]; int anfang, ende; int summe; CTupel *temp_tupel; //Anzahl der Tupel berechnen for (int i=0; i<8; i++) { if (varianten[i]) faktor++; } tupel_anzahl=faktor*(ende-anfang+1); //Berechnungs-Array Speicher zuweisen berechnung=new SAufgabe[tupel_anzahl]; //Berechnungs-Array mit den Werten füllen l=0; for (int i=anfang; i<=ende; i++) { for (int j=0; j<8; j++) { if (varianten[j]) { //Startwerte ermitteln start[0]=int (j&1)+1; start[1]=int (j&2)/2+1; start[2]=int (j&4)/4+1; temp_tupel=new CTupel(4,start,i); berechnung[l].tupel=*temp_tupel; berechnung[l].zugeteilt=false; delete temp_tupel; l++; } } } //Erste Tupel berechnen lassen for (int i=0; i<thread_anzahl_max && i<tupel_anzahl; i++) { //start zusammenbasteln start[0]=berechnung[i].tupel.get_glied(1); start[1]=berechnung[i].tupel.get_glied(2); start[2]=berechnung[i].tupel.get_glied(3); berechnung[i].zugeteilt=true; new CTupelBaum (start,berechnung[i].tupel.get_glied(0),i,Form1); } }
void __fastcall TForm1::ThreadBeendet(TMessage Message) { short start[3]; AnsiString tupel_ausg, temp_string; int naechste_aufgabe=-1; int thread_aktuell; SAufgabe berechnet; //Thread-Zähler dekrementieren thread_anzahl--; //Berechnete Aufgabe suchen berechnet=berechnung[Message.WParam]; //Ergebnis ausgeben //Ausständige Aufgabe suchen for (int i=0; i<tupel_anzahl && naechste_aufgabe==-1; i++) { if (!berechnung[i].zugeteilt) naechste_aufgabe=i; } //Falls noch ein Tupel übrig, dieses berechnen if (naechste_aufgabe!=-1 && !abbrechen) { //start zusammenbasteln start[0]=berechnung[naechste_aufgabe].tupel.get_glied(1); start[1]=berechnung[naechste_aufgabe].tupel.get_glied(2); start[2]=berechnung[naechste_aufgabe].tupel.get_glied(3); berechnung[naechste_aufgabe].zugeteilt=true; thread_anzahl++; new CTupelBaum (start,berechnung[naechste_aufgabe].tupel.get_glied(0),naechste_aufgabe,Form1); } //Falls kein ausständiges Tupel, Berechnung beenden else if (thread_anzahl==0) { //Berechnungsdauer ausgeben zeit_ende=Time(); Label7->Caption=(zeit_ende-zeit_start); //Aufräumen abbrechen=false; momentane_berechnung=false; delete[] berechnung; } }
Dieser Quellcode ist zwar (endlich
) wieder lauffähig und tut auch genau das, was er tun soll. Nur halt wieder auf nur einem Kern.
Womit wird wiederum am Anfang wären...
Und da ich lernfähig bin: Das gesamte Projekt
Noch ein Frage am Rande: Was ist der Vorteil von SendMessage gegenüber PostMessage? Ich finde, dass dem Prinzip des Versenden von Nachrichten PostMessage irgendwie besser enstpricht als SendMessage.
Btw, ich habe vorsichtshalber beides probiert.MfG benediktibk
-
Hi,
bezüglich PostMessage() vs. SendMessage():
Der Unterschied als solches ist klar, denke ich. SendMessage wartet, bis die Botschaft verarbeitet wurde, PostMessage nicht,
Grundsätzlich hast Du recht. PostMessage ist normalerweise die sinnvollere Methode, bei der Kommunikation zwischen Thread und Application. Aber wenn man PostMessage verwendet, muß man dafür sorgen, dass das Objekt noch existiert, dessen Zeiger man in der Botschaft versendet.
Das wäre bei der Verwendung von PostMessage und FreeOnTerminate = true nicht mehr gegeben. Der Thread könnte bereits beendet sein und das Ergebnisobjekt somit bereits gelöscht sein, bevor die Botschaft überhaupt von der Application verarbeitet wird. Deswegen in diesem Fall SendMessage.Den Rest muss ich mir später anschauen...
-
Ok, nachdem ich das jetzt in eine lauffähige Fassung gebracht habe, hat sich meine Befürchtung bestätigt: Die Laufzeit der Threads ist viel zu kurz um sinnvoll Threads einsetzen zu können. Nicht mal auf meiner Single-Core-Maschine wird eine CPU-Last von 100 Prozent erreicht. Die meiste CPU-Zeit verbrät der Hauptthread bei der Erzeugung der Threads. Ich hab versucht, ein bißchen zu optimieren, aber die Laufzeit ist und bleibt viel zu kurz.
Das Konzept muss also überarbeitet werden. Ich würde nur wenige Threads erzeugen, die dann aber mehrere Tupel berechnen müssen. Wenn zB 1000 Ergebnisse zu berechnen sind, dann auf 4-Kern Maschine 4 Threads erzeugen, die jeweils 250 Ergebnisse berechen.
Nachtrag zu SendMessage() vs. PostMessage(): Da in Deiner aktuellen Version tatsächlich nur 2 Integerwerte übergeben werden, ist die Verwendung von PostMessage kein Problem.
-
Bei kleinen Tupeln mag die Laufzeit zu gering für eine Leistungssteigerung durch mehrere Threads sein, jedoch nicht bei größeren, da der Aufwand stark exponentiell ansteigt. Die Tupel ab 1-1-1-150 benötigten zum Beispiel bereits eine deutlich spürbare Zeit, und bei 1-1-1-250 ist es fast eine Sekunde.
Nachdem ich es nicht glauben wollte, habe ich noch ein sehr komplexes Programm geschrieben, in welchem ich beliebig viele solcher Threads erzeuge:
void __fastcall Schleife1::Execute() { while(true); }
Und siehe da, 100% CPU-Last!
Demenstprechend wäre das Problem also gelöst, ich muss nur mein Konzept ändern.
Danke für die Unterstützung,
mfg benediktibk
-
Na ja, bei dieser vorgehensweise reicht es, genau so viele Threads zu erzeugen, wie Prozessoren / Kerne vorhanden sind, um 100% Last zu erzeugen.
Für die neue Version:
Man könnte zB den Thread so gestalten, dass er alle Ergebnisse eines Strangs berechnet. Also zB einen Thread für alle 1-1-1-x Berechnungen. Möglicherweise sollte man diesem noch einen konfigurierbaren Wertebereich spendieren, damit man, bei nur einem Strang, mit großem Wertebereich, die Berechnungen entsprechend verteilen kann.
-
In etwa so habe ich es jetzt auch gelöst, da ich zuerst alle Tupel berechne und in ein Aufgaben-Array speichere.
struct SAufgabe { CTupel tupel; int tupel_id; };
Von diesem Aufgaben-Array nehme ich mir dann, je nach Thread-Anzahl, jede vierte, fünfte, sechste,... Aufgabe heraus und speichere sie in ein anderes Aufgaben-Array. Dieses übergebe ich dann dem Thread.
Je nach Anzahl der Threads und Varianten kommt also genau das heraus, was du vorgeschlagen hast.
Das ursprüngliche Problem besteht aber immer noch.
Bei dieser Execute-Methode, bekomme ich die üblichen ~25% CPU-Last:void __fastcall TeilBerechnung::Execute() { int ergebnis; for (int i=0; i<aufgaben_groesse; i++) { if (TeilAufgabe[i].tupel_id!=-1) { //Berechnung durchführen if (TeilAufgabe[i].tupel.NaechstesTupelUnten_pruefen()) ergebnis=TupelAst(TeilAufgabe[i].tupel, true); else ergebnis=TeilAufgabe[i].tupel.Produkt(); //Botschaft versenden, dass die Berechnung beendet ist PostMessage(Hauptthread->Handle,CM_ERGEBNIS,TeilAufgabe[i].tupel_id,ergebnis); } } //Speicher der TeilAufgabe freigeben delete[] TeilAufgabe; //Melden, dass der Thread beendet ist PostMessage(Hauptthread->Handle,CM_THREADBEENDET,0,0); }
Bei dieser 100%:
void __fastcall TeilBerechnung::Execute() { int ergebnis; while(true); //Speicher der TeilAufgabe freigeben delete[] TeilAufgabe; //Melden, dass der Thread beendet ist PostMessage(Hauptthread->Handle,CM_THREADBEENDET,0,0); }
An den Einstellungen und der Konfiguration der Threads kann es nun also nicht liegen. Ich habe mich trotzdem noch ein bisschen mit der Thread-Priorität gespielt, jedoch ohne Erfolg.
Meine Vermutung ist nun, dass das Problem in der Rekursion liegt, welche sich hinter TupelAst versteckt. Könnte das sein?
Edit: Noch eine Vermutung: Liegt es vielleicht am Speicherzugriff, welcher durch TeilAufgabe zustande kommt? Für das Array reserviere ich den Speicher nämlich mithilfe von new.
Edit2: Ich habe mich noch ein bisschen mit der Thread-Anzahl gespielt, und siehe da: Bei nur einem Thread bekomme eindeutig am schnellsten die Ergebnisse (Bei x=4 bis x=80), nämlich in 0 Sekunden. 2 Threads benötigen 3 Sekunden, 4 oder mehr 4 Sekunden.
Wenn ich im Taskmanager noch explizit nur einen CPU-Kern zuweise, erreiche auch absolute Spitzenergebnisse, fast so schnell, wie die ursprüngliche Ein-Kern-Anwendung.
Daraus folgerte die 3. Vermutung: Spuckt mir der Scheduler in die Suppe?
-
Hmpf, dass verstehe ich dann jetzt irgenwie nicht. Kannst Du das Projekt noch mal hochladen? Ich würd mir das gerne noch mal ansehen.
-
Nun denn, füllen wir die Rapidshare-Server
: Projekt
-
#include "U:\Programme\Tupelberechnung\Version OOP_MT mit Botschaften 2\header\ctupel.h"
Ist das der Header aus dem Headerverzeichnis, oder ein anderer?
-
Ja, das ist er. Beim Inkludieren zickte das Ding irgendwie herum, indem es sich immer beschwerte, dass CTupel nie deklariert wurde. Die ctupel.h im selben Ordner deklarierte CTupel aber. Nachdem ich testweise einmal den vollständigen Pfad einfügte, lief das Ding. Auch nachdem ich Änderungen in beiden (ctupel.h und ctupel.cpp) durchführte, funktionierte alles noch, weshalb es die richtige Datei sein muss.
-
Schlußendlich kann man nur sagen, es passiert zu wenig in den Threads und zu viel im Forumular... Auch die Zeit, die für das Einfügen ins Memo nötig ist, ist nicht zu unterschätzen...
Ich hab hier mal ein paar Kleinigkeiten geändert:
[/EDIT] Link entfernt [EDIT]
Ist auf Basis deiner ersten Fassung, besteht aber nur noch aus 2 Units. Das MainForm und die grundsätzliche Threadsteuerung sollten so ganz gut funktionieren. Ich hatte allerdings nicht so viel Zeit und so ist es kein schöner Code geworder und in der Execute Methode wird zur Zeit nur eine Berechnugn simuliert. Ich hab aber die Header aus dem neuen Projekt schon dazu kopiert, so dass Du nur noch zwischen 'Simulation Beginn' und 'Simualtion Ende' anpassen musst.Um die Auswirkungen der Anzeige der Daten zu demonstriern
Direkt unter 'Simulation Beginn', in der Execute ist eine Schleife. Trage dort 1000 als Max-Wert ein, starte das Projekt und trage bei Ende 15000 ein und schau dir mal die Auswirkungen der neuen Auswahl 'Ergebnisse zeigen' an.Ich hab zwar hier nur einen Single-Core und kann deshalb nicht richtig testen, es sollten so aber alle Kerne genutzt werden.