Java Problem beim Ablauf von mehreren Threads

darton

Lt. Junior Grade
Registriert
Okt. 2004
Beiträge
282
Hallo!
Ich lese im Moment ein paar Dateien mit Java aus. Für jede Datei soll es einen eigenen Thread geben. Falls beim Einlesen irgendwelche Fehler auftreten, soll der fehlerhafte Teil der Datei in einem JPanel angezeigt werden und man hat die Möglichkeit, die Stelle zu korrigieren. Mein Problem ist nun, dass mehrere Threads gleichzeitig einen Fehler werfen können und viele Fenster auf einmal aufblitzen, wo man etwas korrigieren soll. Es soll natürlich immer nur ein Fenster aufgerufen werden. Ich habe schon an eine globale Liste gedacht, wo man immer den ersten Eintrag rauspickt, aber find die Lösung nicht so schön. Habt ihr da vielleicht bessere Ideen?
 
Haben die Dateien (sagen wir mal 20) einen fixen Stand bei Ausführungsbeginn Deiner Anwendung? Wenn ja, dann lass Dein Programm drauf rumlaufen und alle gefundenen Fehler in einer separaten Liste speichern (so wie Du es Dir schon überlegt hast). Ein Anwender kann dann in dieser Liste Fehler raussuchen, an die entsprechende Stelle springen und diesen korrigieren.

Etwas abstrakter gesprochen: Threads sind gut für Programme, damit sie bspw. die Last besser verteilen können. Aber immer dann, wenn ein "Ereignis" oder ein Zustand dazu führt, dass eine Nutzerinteraktion erfolgen muss, wird es aus Usability-Sicht schwierig.
 
Kannst ja die Panels in eine Queue packen und nacheinander öffnen, immer wenn eins beantwortet wurde.
 
Ah ja, das klingt gut und das sollte auch funktionieren. Dann werde ich die Fehler erst ganz zum Schluss behandeln, wenn alle Dateien eingelesen worden sind.
 
Zu aller erst: Für jede Datei einen eigenen Thread?
Ist das nicht total ineffizient, weil dann die Festplatte sau viele parallele Zugriffe auf einmal bekommt?
Und mehr Threads als (virtuelle) CPU-Kerne bringen eh so gut wie keinen Performance-Vorteil.

Zu deinem Problem:
Ich würde es nicht so machen, dass die Threads direkt die "Fehlerkorrigierungsfenster" aufmachen, sondern die Fehler in einer Liste abspeichern und ein separater Thread sich die Liste anguckt und entsprechend die GUI zeichnet, zB mit einer Art Tabelle, wo jede Zeile eine Fehlerbeschreibung und dazugehörige Buttons erzeugt.

Gruß TImo
 
OK, zu der Sache mit den Threads: Sollte ich dann vielleicht nur 2-4 Threads gleichzeitig laufen lassen und wenn das fertig ist, die nächsten 2-4 starten lassen?
 
Du kannst ja über Runtime.getRuntime().availableProcessors(); die Anzahl der CPU-Kerne ermitteln und entsprechend viele Threads erzeugen.
Und ja, ich würde es auch so machen, dass dann immer n Threads n Dateien verarbeiten und wenn ein Thread fertig ist, springt er zu Datei Nummer i*n+threadNummer

Gruß Timo
 
Die Semaphore ist dein Freund.

Ich hätte allerdings auch die Lösung mit der globalen Liste bevorzugt.
 
Semaphoren sind hier denkbar unangebracht... nichts für ungut. Man KANN es damit zwar umsetzen, aber was er eigentlich sucht dürfte viel eleganter mit dem Master/Worker-Prinzip für das Abarbeiten der Dateien zu lösen sein und für die Ausgabe:
Falls es wirklich nur 1 Fenster geben soll, wie bereits gesagt mit einer Queue.

Das Ding ist nämlich: Warum soll man die anderen Prozesse zwingen anzuhalten, wenn mehr als 1 Prozess eine Ausgabe machen will? Die Zeit kann man sich sparen ;)

Und zum Schluss: Achte darauf, dass deine Queue Thread-Safe ist! Also keine ArrayList verwenden, sondern eher ein Vector.
 
benneque schrieb:
Achte darauf, dass deine Queue Thread-Safe ist! Also keine ArrayList verwenden, sondern eher ein Vector.

Das versteht sich wohl von selbst! Aber dass einem Vector heutzutage überhaupt noch in den Sinn kommt!? Eine ArrayList lässt sich übrigens auf triviale Weise thread-safe machen. Doch seit Java 5 gibt es speziell für Concurrency optimierte Datenstrukturen. Davon sollte man Gebrauch machen!

Ich würde das Producer/Consumer Pattern verwenden. Ein Thread ermittelt die Dateien und schreibt deren Pfad in eine Queue. Dort holen sich dann die Consumer-Threads die Dateien ab und führen die eigentliche Verarbeitung durch. Deren Ergebnis wird in eine weitere Queue geschrieben, die dann ein weiterer Thread abarbeiten/darstellen kann.
Ergänzung ()

yoT!mO schrieb:
Und ja, ich würde es auch so machen, dass dann immer n Threads n Dateien verarbeiten und wenn ein Thread fertig ist, springt er zu Datei Nummer i*n+threadNummer

Es wird einfach die nächste Datei aus der Queue genommen, solange bis keine Dateien mehr vorhanden sind. Man muss natürlich prüfen, ob die Ermittlung der Dateien abgeschlossen ist und ansonsten warten, bis das der Fall ist, um zu wissen, ob noch weitere Dateien hinzugefügt werden können.
Ergänzung ()

darton schrieb:
OK, zu der Sache mit den Threads: Sollte ich dann vielleicht nur 2-4 Threads gleichzeitig laufen lassen und wenn das fertig ist, die nächsten 2-4 starten lassen?

Threads zu erzeugen ist relativ teuer. Du solltest einmal die zur Verarbeitung notwendigen Threads starten und diese dann die Queue abarbeiten lassen.
Ergänzung ()

darton schrieb:
Ah ja, das klingt gut und das sollte auch funktionieren. Dann werde ich die Fehler erst ganz zum Schluss behandeln, wenn alle Dateien eingelesen worden sind.

Kann man so machen, aber was spricht denn dagegen die Liste mit den fehlerhaften Dateien in Echtzeit zu aktualisieren? So könnte der Benutzer schon anfangen, die Fehler zu beseitigen, während die Verarbeitung noch läuft!
 
soares schrieb:
Das versteht sich wohl von selbst! Aber dass einem Vector heutzutage überhaupt noch in den Sinn kommt!? Eine ArrayList lässt sich übrigens auf triviale Weise thread-safe machen. Doch seit Java 5 gibt es speziell für Concurrency optimierte Datenstrukturen. Davon sollte man Gebrauch machen!
Die Sache ist nur die, dass ich eine Hashset für die gesammelten Daten benutze, da ich keine doppelten Einträge möchte. Die muss ich dann wohl manuell mit synchronized-Befehlen thread-safe machen.

soares schrieb:
Threads zu erzeugen ist relativ teuer. Du solltest einmal die zur Verarbeitung notwendigen Threads starten und diese dann die Queue abarbeiten lassen.
Meinst du das so, dass ich quasi nur so viele Threads wie Prozessorkerne erzeuge und wenn einer fertig ist, dann starte ich ihn einfach mit einer anderen Datei neu anstatt wieder einen neuen Thread zu erzeugen?
 
darton schrieb:
Die Sache ist nur die, dass ich eine Hashset für die gesammelten Daten benutze, da ich keine doppelten Einträge möchte. Die muss ich dann wohl manuell mit synchronized-Befehlen thread-safe machen.

Die Problemstellung ist nicht sehr detailliert. Wo gibt es denn mögliche Dupletten? Werden die Dateien mehrfach verarbeitet?

Zur Synchronisierung gibt es notfalls java.util.Collections#synchronizedSet(). Aber wie gesagt, wo Java bereits so viele schöne Sachen für die parallele Programmierung anbietet, sollte man davon auch Gebrauch machen, wenn möglich.

darton schrieb:
Meinst du das so, dass ich quasi nur so viele Threads wie Prozessorkerne erzeuge und wenn einer fertig ist, dann starte ich ihn einfach mit einer anderen Datei neu anstatt wieder einen neuen Thread zu erzeugen?

Nein, die Threads werden einmal gestartet und laufen dann solange bis alle Einträge verarbeitet sind. Java bietet hierfür seit Version 5.0 fertige Klassen.

Hier mal ein Beispiel-Gerüst:

Code:
ExecutorService es = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
CompletionService<Object> ecs = new ExecutorCompletionService<Object>(es);

for (File file : files) {
  ecs.submit(new FileTask(file));
}

Die Verarbeitung geschieht durch eine vorgegebene maximale Anzahl von Threads, um deren Verwaltung Du Dich nicht kümmern musst. Lediglich die eigentliche Verarbeitung muss implementiert werden (hier durch FileTask, einer java.util.concurrent.Callable Implementierung). Dort liest Du die Datei ein, führst Deine Logik durch und reichst das Ergebnis zur weiteren Verarbeitung weiter, z.B. an einen weiteren ExecutorService, der die Daten dann in der GUI anzeigt.

Ich würde aber wie gesagt, bereits die Ermittlung der Dateien parallel laufen lassen, damit sobald die erste Datei gefunden ist, die eigentliche Verarbeitung losgehen kann.
 
Zuletzt bearbeitet:
soares schrieb:
Die Problemstellung ist nicht sehr detailliert. Wo gibt es denn mögliche Dupletten? Werden die Dateien mehrfach verarbeitet?
In den Dateien können unter Umständen doppelte Daten auftreten, deswegen die Set.

Ich werde mich dann mal mit diesen ExecutorServices vertraut machen.
 
soares schrieb:
die Threads werden einmal gestartet und laufen dann solange bis alle Einträge verarbeitet sind.

Was man allerdings entsprechend coden muss, in dem man z.B. den Service anhält, wenn alle Dateien verarbeitet sind. Insofern war dieser Satz missverständlich.
Ergänzung ()

darton schrieb:
In den Dateien können unter Umständen doppelte Daten auftreten, deswegen die Set.

Ohne jetzt die Anforderungen genau zu kennen, würde ich die Fehler per Datei darstellen, z.B. in einer Tabelle oder Liste (die dynamisch aktualisiert wird).

Man startet die Analyse. Alle fehlerhaften Dateien werden in einer Tabelle angezeigt. Klickt man auf einen Dateieintrag, werden alle in der Datei gefundenen Fehler in einer View dargestellt.
 
Also ich find das Thema mit dem ExecutorService ziemlich schlecht dokumentiert im Netz, weshalb ich dich nochmal was fragen muss.
Wofür braucht man den CompletionService? Im Internet wurden fast alle Beispiel nur durch den ExecutorService gelöst.
 
darton schrieb:
Wofür braucht man den CompletionService? Im Internet wurden fast alle Beispiel nur durch den ExecutorService gelöst.

Der CompletionService kapselt Executor und Queue. Wenn man die Ergebnisse eines Executors unmittelbar weiterverarbeiten möchte, dann ist das die eleganteste Lösung. Man muss nicht zwingend so vorgehen.

Bei diesem Problem passt es. Sobald eine Datei verarbeitet wurde, kann das Ergebnis weiter gereicht werden, z.B. zur Darstellung in einer Tabelle.

Je nach Lage der Dinge kann man sich überlegen, ob man die komplette Fehlerbehandlung sofort (im Task) durchführt. D.h. wenn Fehler festgestellt werden, alle notwendigen Informationen ermittelt, und diese als Future zurückgibt. Oder lediglich zurückgibt, dass eine Datei fehlerhaft ist und erst dann die Fehlerbehandlung durchführt, wenn der Benutzer auf einen Eintrag in der Tabelle mit den fehlerhaften Dateien klickt. Wenn Speicher kein Problem ist, ist die erste Variante effektiver. Aber ich schweife ab ;)
 
Also ich verwede den CompletionService, um die Tabelle in der GUI zu aktualisieren?
Könnte man das nicht genauso gut mit dem Observer/Observable Prinzip machen? Also der Observer schaut sich die ganze Zeit die Queue an und wenn sich etwas ändert, dann ändert er auch die GUI.

Obwohl...hm...der CompletionService unterbricht aber keine Threads, wenn er die GUI nachlädt, oder?
 
Zuletzt bearbeitet:
IMHO ja. Aber es kommt natürlich darauf an, was Du erreichen möchtest bzw. was Dir wichtiger ist. Bei Deinem Problem wird es vermutlich keine große Rolle spielen, da die Verarbeitung einer Datei nach Feststellung der Fehlerhaftigkeit wahrscheinlich nicht viel Zeit in Anspruch nimmt.

Ich versuche Aufgaben immer möglichst weit zu unterteilen und damit möglichst gut parallelisierbar zu machen. In diesem Fall würde das bei einer Trennung bedeuten, dass man schneller weiß, welche Dateien fehlerhaft sind. Möglicherweise will der Benutzer sich nur bestimmte Dateien anschauen und könnte somit schneller die für ihn interessanten Dateien aus der Liste picken, wenn man das Ganze so implementiert, dass ein Aufruf sofort die Verarbeitung der fehlerhaften Datei startet (sollte diese in der Queue noch nicht abgearbeitet worden sein).

Ev. Overkill für Dein Problem. Hat man sich einmal eine solche Arbeitsweise angewöhnt, ist das einfach drin und dann natürlich eine Frage der Aufgabenstellung und Ressourcen wie weit man hier geht. Ich würde wie gesagt auch die Dateisuche bereits in einem eigenen Prozess laufen lassen, sodass sobald die erste Datei bekannt ist, die eigentliche Verarbeitung beginnen kann.
Ergänzung ()

darton schrieb:
Also ich verwede den CompletionService, um die Tabelle in der GUI zu aktualisieren?
Könnte man das nicht genauso gut mit dem Observer/Observable Prinzip machen? Also der Observer schaut sich die ganze Zeit die Queue an und wenn sich etwas ändert, dann ändert er auch die GUI.

Obwohl...hm...der CompletionService unterbricht aber keine Threads, wenn er die GUI nachlädt, oder?

Über den CompletionService werden die Dateien/Ergebnisse bekannt gemacht, die (in diesem Fall) in der GUI dargestellt werden sollen. Man kann das natürlich auch anders lösen. Der CompletionService verwendet intern eine Queue, und gibt deren Elemente zurück, sobald diese dort auftauchen. Ist insofern Dein Observer und Du musst dich lediglich um die Anwendungsdetails kümmern, nicht um das Gerüst.

Was für Threads sollen denn unterbrochen werden? Über den CompletionService kommst Du wie gesagt an die Ergebnisse Deiner Tasks ran. Was Du damit anstellst, bleibt Dir überlassen.
 
Ich habe wieder ein kleines Problem. Meine Callable-Klasse wirft manchmal ein paar Exceptions. Aber anscheinend wirkt sich das nicht aus. Ich möchte gerne die Exceptions angezeigt bekommen, aber in der Konsole steht einfach nichts.
Hier mal ein Beispiel:
Code:
public class CompletionServiceTest {

	public static void main(String[] args) {
		ExecutorService taskExecutor = Executors.newFixedThreadPool(3);
		CompletionService<CalcResult> taskCompletionService = new ExecutorCompletionService<CalcResult>(
				taskExecutor);

		int submittedTasks = 5;
		for (int i = 0; i < submittedTasks; i++) {
			taskCompletionService.submit(new CallableTask(10));			
		}		
		taskExecutor.shutdown();
	}
}


public class CallableTask implements Callable<CalcResult> {
    int input;

    CallableTask(int v1){        
        input = v1;
    }
    
	public CalcResult call() throws Exception {		
		if (input > 10)
			throw new RuntimeException("Error");
		for (int i = 0; i<= 10; i++)
			System.out.println(i);
		return null;
	}
}


public class CalcResult {
     int result ;

     CalcResult(int l){
         result = l;
     }
}
Da wo die Exception geworfen wird, hat die Variable input immer den Wert 10. Wieso wird dann keine Exception angezeigt?
 
darton schrieb:
Da wo die Exception geworfen wird, hat die Variable input immer den Wert 10. Wieso wird dann keine Exception angezeigt?

Zum Einen wird in dem Beispiel niemals eine Exception geworfen, weil input > 10 niemals eintritt.

Aber was Du eigentlich wissen willst ist, dass die Exceptions beim Zugriff auf die Ergebnisse der Taskverarbeitung weiter gereicht werden.

Code:
public static void main(String[] rArgs)
{
    ExecutorService taskExecutor = Executors.newFixedThreadPool(3);
    CompletionService<CalcResult> taskCompletionService =
        new ExecutorCompletionService<CalcResult>(taskExecutor);

    int submittedTasks = 5;

    for (int i = 0; i < submittedTasks; i++)
    {
        taskCompletionService.submit(new CallableTask(10));
    }

    try
    {
        for (int i = 0; i < submittedTasks; i++)
        {
            CalcResult calc = taskCompletionService.take().get();

            // do something with the result
        }
    }
    catch (InterruptedException ex)
    {
        Thread.currentThread().interrupt();
    }
    catch (ExecutionException ex)
    {
        // handle exception here
        ex.printStackTrace();
    }

    taskExecutor.shutdown();
}
 
Zurück
Oben