Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]


Groups > de.comp.lang.java > #13037

Re: JUnit-Tests funktionieren nur manchmal: Synchronisations-Problem

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>

Show all headers | View raw


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 | NextPrevious in thread | Next in thread | Find similar


Thread

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