Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]
Groups > de.comp.lang.java > #13037
| From | Marcel Mueller <news.5.maazl@spamgourmet.org> |
|---|---|
| Newsgroups | de.comp.lang.java |
| Subject | Re: JUnit-Tests funktionieren nur manchmal: Synchronisations-Problem |
| Date | 2016-08-25 11:03 +0200 |
| Organization | MB-NET.NET for Open-News-Network e.V. |
| Message-ID | <npmc9e$stq$1@gwaiyur.mb-net.net> (permalink) |
| References | <e25pnrFqe4gU1@mid.individual.net> |
On 24.08.16 16.36, Christian H. Kuhn wrote:
> Immer noch die Schachuhr.
[langer code...]
Das ist mir jetzt etwas zu aufregend, alles durchzugehen, aber
vielleicht ein paar Tips.
> 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();
> }
> }
> }
Du musst mit der notify() aufpassen wie ein Höllenhund, denn wenn der
Zielthread gerade mit etwas anderem beschäftigt ist, als mit wait(),
geht selbiges ins Leere. Daher die Booleans. Aber es ist erstaunlich
Fettnäpfchenreich Bedingungsvariablen korrekt zu implementieren.
So ist die Funktion waitForNotify() so wie sie implementiert ist, m.E.
nicht fehlerfrei nutzbar. Sie löscht als erstes notified, und dann
wartet sie. Das notwendige Pattern ist aber:
- notified einmalig initialisieren (eigentlich sogar optional),
- jetzt erst alle thread anstupsen, die notified setzen können,
- und dann wait(),
- notified löschen
- und dann das Ereignis, das zu notified gehört, verarbeiten.
Das kann man mit obiger Funktion nicht erreichen, weil man ja nicht
weiß, wann es so weit ist, die anderen Threads arbeiten zu lassen,
respektive notified unkontrolliert gelöscht wird.
In jedem Fall sind Funktionsname und Funktionsinhalt inkonsistent. Die
Funktion wartet nicht nur.
Die Regel ist, wer notified löscht, muss auch die damit verbundenen
Ereignisse verarbeiten. Wenn das Ereignis also bereits vor Aufruf der
Funktion eintritt, geht es ins Nirwana.
Das ist letztlich, wie bei einem Interrupt-Handler. Der IRQ tritt ein,
ein Handler wird aufgerufen, und /der/ setzt das Flag zurück,
üblicherweise /bevor/ er mit seiner Arbeit der Abarbeitung beginnt, denn
es könnten während der Verarbeitung ja schon weitere Ereignisse
reinpurzeln. Falls er die nach dem Rücksetzen doch schon mit bearbeitet
hat, wird er im schlimmsten Fall noch genau ein weiteres mal ohne
Arbeitsvorrat aufgerufen.
> 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.
So schaut's.
> 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();
> }
> }
> }
Hier passt das Pattern im Prinzip.
Wobei natürlich auf jedes beliebige "notified" gewartet wird. Der Name
ist also nicht sonderlich sprechend.
> 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.
Hört sich soweit korrekt an, aber überprüfe nochmal die Zugriffe auf die
Bedingungsvariable notified. Normalerweise will man Bedingungsvariablen
nur an genau einer Stelle im Code setzen und nur an genau einer anderen
Löschen. Alles andere ist zumindest potentiell gefährlich, weil dann
verschiedene Bedingungen sich gegenseitig stören können.
(Man muss ein bisschen mit den Begrifflichkeiten hier aufpassen, eine
Bedingung ist nicht notwendigerweise durch ein einziges Boolean
repräsentiert, es kann auch komplexer sein. Und in dem Fall gibt es dann
i.a. auch mehrere Zugriffe auf die Variablen. Das scheint mir aber hier
nicht gegeben.)
> 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.
Wie gehören "changed" und "notified" zusammen? Ich habe jetzt keine
Muße, den kompletten Code durchzulesen.
> Soweit die Theorie. Beim Ausführen der Tests kommt es unregelmäßig zu
> Fehlern. Tests bleiben einfach mal stehen, weil notified false bleibt.
Wenig überraschend, falls waitForNotify() dabei jemals durchlaufen wird.
> Da wird ein update() verschluckt. Im Debugger funktioniert alles immer
> bestens.
Das alte Spiel mit Race-Conditions. Die kann man nicht Debuggen. Selbst
ein Logging, kann manchmal durch die dafür erforderliche Synchronisation
schon dafür sorgen, dass es geht.
> 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.
Bei Race Conditions weiß man nie, wann sie Zünden. Das Kann sein, wenn
schönes Wetter ist, wenn in China ein Sack Reis platzt oder auch nur
einmal in der Lebensdauer des Universums.
> Ich halte das für ein deutliches Zeichen, dass da was mit der
> Nebenläufigkeit nicht stimmt.
Auf jeden Fall.
> Ich finde den Fehler aber nicht. Wer winkt
> mit dem Zaunpfahl?
Guck Dir mal die Bedingungsvariablen genau an (Verwendunganachweis),
Nicht nur in waitForNotify(). Wie gesagt, eine Bedingung sollte
normalerweise nur an genau einer Stelle zurückgesetzt werden, nämlich
da, wo sie behandelt wird.
Marcel
Back to de.comp.lang.java | Previous | Next — Previous in thread | Next in thread | Find similar
JUnit-Tests funktionieren nur manchmal: Synchronisations-Problem "Christian H. Kuhn" <qno-news@qno.de> - 2016-08-24 16:36 +0200
Re: JUnit-Tests funktionieren nur manchmal: Synchronisations-Problem Lothar Kimmeringer <news200709@kimmeringer.de> - 2016-08-24 22:30 +0200
Re: JUnit-Tests funktionieren nur manchmal: Synchronisations-Problem "Christian H. Kuhn" <qno-news@qno.de> - 2016-08-26 19:10 +0200
Re: JUnit-Tests funktionieren nur manchmal: Synchronisations-Problem Marcel Mueller <news.5.maazl@spamgourmet.org> - 2016-08-25 11:03 +0200
Re: JUnit-Tests funktionieren nur manchmal: Synchronisations-Problem "Christian H. Kuhn" <qno-news@qno.de> - 2016-08-26 20:21 +0200
csiph-web