Forums Neueste Beiträge
 

JUnit-Tests funktionieren nur manchmal: Synchronisations-Problem

24/08/2016 - 16:36 von Christian H. Kuhn | Report spam
Hallo Gemeinde,

Immer noch die Schachuhr.
Projekt: https://www.qno.de/gitweb/?p=qchess...;a=summary
Snapshot unter
https://www.qno.de/gitweb/?p=qchess...a77;sf=tgz

Alles lief, und da musste ich doch was àndern :-)

Ich glaubte, die Multithreading-Probleme gelöst zu haben. Um die
Systemlast ein klein wenig zu reduzieren, bin ich auf die Idee gekommen,
in der QChessClock in notifyObservers() auf changed zu prüfen. changed
wird gesetzt, wenn von außen ein Knopf gedrückt oder die Bedenkzeit
veràndert wird. Bei jedem Aufruf von notifyObservers wird außerdem
überprüft, ob sich die anzuzeigende (also auf Sekunden gerundete)
Bedenkzeit geàndert hat oder ob ein Blàttchen gefallen ist. Auch dann
wird changed gesetzt. Nur wenn sich etwas geàndert hat, wird das Objekt
zum Übertragen des Status gefüllt, und nur dann werden die Observer mit
diesem Objekt benachrichtigt.

Dies führt wie erhofft dazu, dass Observer nicht mehr alle 20 ms,
sondern nur bei einer Benutzeraktion bzw. beim Umspringen der
Sekundenanzeige benachrichtigt werden. In der Swing-GUI funktioniert
auch weiterhin alles.

Schwierigkeiten machen die JUnit-Tests. Die Testklasse implementiert die
Funktionen einer GUI, um die zu testende Klasse anzusprechen. Die
Testklasse registriert sich als Observer. Bislang wurde alle 20 ms die
Update-Funktion von notifyObservers() aufgerufen:

@Override
public final void update(final QChessClockSTO _sto) {
synchronized (this) {
state = _sto.getStatus();
(...) // weitere übertragene Daten
notified = true;
notifyAll();
}
}

Im Test wurde eine Aktion auf der zu testenden Klasse aufgerufen, mit
waitForNotify() auf den eintreffenden update() gewartet und dann die
entsprechenden asserts vorgenommen. Ob ein update() verpasst wurde oder
nicht, war nicht wichtig, der nàchste kam ja 20 ms spàter.

private void waitForNotify() throws InterruptedException {
synchronized (this) {
notified = false;
while (!notified) {
wait();
}
}
}

Durch den Test auf changed erfolgt jetzt nur noch ein einziger update()
als Reaktion auf die Benutzeraktion. Wird der verpasst, kommt kein
weiterer mehr, und waitForNotify hàngt in der Endlosschleife. Also
folgende Hilfsfunktion:

private void pressButton(final Buttons _button) throws
InterruptedException {
synchronized (this) {
notified = false;
switch (_button) {
case LEFT_:
sut.moveButtonPressed(LEFT);
break;
(...) // weitere Knöpfe
default:
sut.resetPressed();
break;
}
while (!notified) {
wait();
}
}
}

Im Test wird pressButton() aufgerufen, danach die Asserts durchgeführt.

Ich glaubte, folgendes erreicht zu haben: pressButton holt sich den
Lock, löscht notified und führt die Benutzeraction an der Uhr aus.
Danach wird die Endlosschleife betreten, der Lock freigegeben und auf
notifyAll() gewartet. Irgendwann kommt der update(). Der braucht den
Lock. Kommt der update, bevor wait() erreicht ist, ist der Lock noch
belegt, und update() muss warten. Bekommt update() den Lock, werden die
Daten aktualisiert, notified wird gesetzt, notifyAll() aufgerufen und
der Lock freigegeben. wait() erkennt den notifyAll(), beendet das
Warten, bekommt den Lock zurück. Die Endlosschleife wird beendet, die
Daten sind aktuell und können mit Assert überprüft werden.

Auf QChessClock benutzen die Benutzer-Zugriffsmethoden und
notifyObservers den gleichen Lock. Wenn ein Knopf gedrückt wird, kann
der Lock für die angeforderte Methode nur erhalten werden, wenn
notifyObservers gerade nicht làuft. Wàhrend diese Methode ausgeführt
wird, an deren Schluss setChanged(true) aufgerufen wird, kann
notifyObservers nicht ausgeführt werden. Erst wenn die
Benutzer-Zugriffsmethode beendet ist, erfolgt der nàchste
notifyObservers. Der sieht changed = true und ruft auf der Testklasse
update() auf.

Soweit die Theorie. Beim Ausführen der Tests kommt es unregelmàßig zu
Fehlern. Tests bleiben einfach mal stehen, weil notified false bleibt.
Da wird ein update() verschluckt. Im Debugger funktioniert alles immer
bestens. Da ist das update schon am Warten, wenn das wait() erreicht
ist, sobald wait() ausgeführt ist, ist notified true, und alles ist gut.
Im echten Ablauf passiert das aus mir unbekannten Gründen anscheinend nicht.

Diesen Fehler konnte ich nicht beobachten, wenn ich eine Testmethode
einzeln ausführe. Führe ich die Testklasse aus, egal ob in Eclipse oder
mit Gradle, tritt der Fehler an einer anscheinend zufàlligen Testmethode
auf.

Ich halte das für ein deutliches Zeichen, dass da was mit der
Nebenlàufigkeit nicht stimmt. Ich finde den Fehler aber nicht. Wer winkt
mit dem Zaunpfahl?

TIA
QNo
 

Lesen sie die antworten

#1 Lothar Kimmeringer
24/08/2016 - 22:30 | Warnen spam
Christian H. Kuhn wrote:

Bislang wurde alle 20 ms die
Update-Funktion von notifyObservers() aufgerufen:

@Override
public final void update(final QChessClockSTO _sto) {
synchronized (this) {
state = _sto.getStatus();
(...) // weitere übertragene Daten
notified = true;
notifyAll();
}
}

Im Test wurde eine Aktion auf der zu testenden Klasse aufgerufen, mit
waitForNotify() auf den eintreffenden update() gewartet und dann die
entsprechenden asserts vorgenommen. Ob ein update() verpasst wurde oder
nicht, war nicht wichtig, der nàchste kam ja 20 ms spàter.

private void waitForNotify() throws InterruptedException {
synchronized (this) {
notified = false;
while (!notified) {
wait();
}
}
}



Hohl dir den Lock vor dem Aufruf der waitForNotify-Methode. Fuer
detaillierte Analysen wuerde die Nennung der konkreten Klassen
helfen, in denen die Dinge passieren. Ansonsten muesste ich mich
auf die Suche durch deine komplette Sourcebasis machen.


Gruesse, Lothar
Lothar Kimmeringer E-Mail:
PGP-encrypted mails preferred (Key-ID: 0x8BC3CD81)

Always remember: The answer is forty-two, there can only be wrong
questions!

news://freenews.netfront.net/ - complaints:

Ähnliche fragen