Multithread-Anwendung nutzt nur einen Kern
-
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.