05 grudnia 2011

j.u.concurrent.Phaser i wątek onAdvance w Java 7

Każdorazowo, kiedy przygotowywałem klasę wspomagającą rozpoznanie java.util.concurrent.Phaser jej uruchomienie wymagało dodania opcji -ea w konfiguracji uruchomieniowej w Eclipse. W ten sposób mogłem prezentować moje doświadczenia w postaci pojedynczej klasy z użyciem assert. To jednak wymagało ode mnie właściwej konfiguracji dla każdej uruchamianej klasy (!) Wystarczy jednak mała zmiana w konfiguracji JRE i po sprawie!


Podczas mojego ostatniego wystąpienia o zmianach w Java 7 w kontekście programowania współbieżnego, w którym przedstawiłem j.u.c.Phaser, padło pytanie o wątek, który uruchamia metodę protected boolean onAdvance(int phase, int registeredParties). Za pomocą tej metody kontrolujemy dostępność obiektu Phaser. Kiedy zwróci true, Phaser kończy swoje działanie. Dzięki niej, mamy również możliwość uruchomienia akcji podsumowującej fazę, kiedy ostatni wątek w danej fazie zgłosi swoje przybycie, a przed przejściem do kolejnej (jeśli takowa w ogóle nastąpi).

Podczas spotkania nie byłem w stanie odpowiedzieć na pytanie o wątek, w którym będzie uruchomiona metoda onAdvance(), co czynię teraz, w poniższej klasie.
package pl.japila.java7.concurrent.phaser;

import java.util.concurrent.Phaser;

public class PhaserOnAdvanceDemo {

    static class MyPhaser extends Phaser {
        String threadName;

        public void setOnAdvanceThreadName(String threadName) {
            this.threadName = threadName;
        }

        protected boolean onAdvance(int phase, int parties) {
            assert threadName.equals(Thread.currentThread().getName());
            return true;
        }
    }

    public static void main(String[] args) throws Exception {

        final MyPhaser phaser = new MyPhaser();

        assert 0 == phaser.getPhase();
        assert 0 == phaser.getRegisteredParties();
        assert 0 == phaser.getUnarrivedParties();

        final int parties = 2;

        phaser.bulkRegister(parties); // register 2 threads - main's and one more

        assert 0 == phaser.getPhase();
        assert parties == phaser.getRegisteredParties();
        assert parties == phaser.getUnarrivedParties();

        phaser.arrive(); // main thread arrives

        assert 0 == phaser.getPhase();
        assert parties == phaser.getRegisteredParties();
        assert 1 == phaser.getUnarrivedParties();

        Thread t = new Thread() {
            public void run() {
                phaser.arrive(); // this (sub)thread arrives
            }
        };
        phaser.setOnAdvanceThreadName(t.getName());
        t.start();

        t.join(); // wait till t dies

        assert phaser.isTerminated(); // onAdvance returned true, and hence phaser is terminated
        assert 0 > phaser.getPhase(); // phaser is terminated, and hence getPhase() returns negative number
        assert parties == phaser.getRegisteredParties();
        assert 0 == phaser.getUnarrivedParties();
    }

}
Zanim uruchomisz powyższą klasę, zastanów się, jaka może być odpowiedź. Zmiana kolejności uruchomienia phaser.arrive() ułatwia znalezienie odpowiedzi. Wystarczy przenieść linię 36 z phaser.arrive() dla głównego wątku, po t.join() w linii 50. Potrafisz wytłumaczyć dlaczego? Chętnie odpowiem na wyraźną prośbę (jej brak będzie oznaką znajomości odpowiedzi). Dla mnie to teraz taaaaakie oczywiste, ale trzeba było widzieć moją minę na prezentacji! :)

p.s. W nadchodzący piątek, 9 grudnia, występuję w roli prelegenta na konferencji Cracow.mobi z tematem RESTful Android. Będzie to moje pierwsze, androidowe wystąpienie, a 45 minut na prezentację uważam jedynie za możliwość nakreślenia tematu i jestem zmuszony do potraktowania go wyłącznie slajdami. Sugestie odnośnie sposobu i zawartości prezentacji tematu mile widziane.

9 komentarzy:

  1. Zmobilizowalem się, sciągnąłem jave 7, podebugowalem i już wiem.

    OdpowiedzUsuń
  2. Możesz podzielić się odpowiedzią? Chętnie przeczytam, jak inni postrzegają ten temat.

    OdpowiedzUsuń
  3. Z tego co udało mi się wyczytać:

    "When the final party for a given phase arrives, an optional action is performed and the phase advances. These actions are performed *by the party triggering a phase advance*, and are arranged by overriding method *onAdvance(int, int)*, which also controls termination."

    [http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Phaser.html#arriveAndAwaitAdvance()]

    Czyli onAdvance jest wykonywane przez ostatnie 'party' na które czekamy w danej fazie. Co więcej, 'party' nie powinno być utożsamiane z wątkiem:

    "As is the case with most basic synchronization constructs, registration and deregistration affect only internal counts; they do not establish any further internal bookkeeping, so tasks cannot query whether they are registered. (However, you can introduce such bookkeeping by subclassing this class.)"

    - co oznacza, że jeden wątek może pełnić rolę kilku 'parties' na raz (wystarczy że kilka razy zawoła register() i odpowiednią liczbę razy na fazę arrive()), lub - jeszcze skrajniej - dwa wątki mogą w kolejnych fazach rozdzielać między sobą ile 'parties' reprezentują.

    Jeśli się mylę - poprawcie :)

    OdpowiedzUsuń
  4. Podałem zły link (strona jest ta sama, tylko przytoczony tekst jest na górze strony a nie w opisie metody arriveAndAwaitAdvance)

    Pozdrawiam :)

    OdpowiedzUsuń
  5. Jest to trochę dziwne, że onAdvance jest wykonywany w ramach ostatniego wątku, który to "przybył" jako ostatni. Trzeba uważać co się w onAdvance robi, ponieważ kod ten może wpaść w ręce, w które nie powinien. No ale żeby zrobić to inaczej to musiałby Phaser utworzyć nowy wątek i w nim uruchomić onAdvance. No ale to też jest głupie. Mam małe doświadczenie w tej domenie, ale na razie podchodziłbym do tego jak do jakiegoś zagrożenia.

    OdpowiedzUsuń
  6. Brawa Panowie za wytrwałość i rozwiązanie zagadki. Właśnie ostatni wątek zadania, które kończy fazę uruchamia onAdvance. Dla mnie (teraz, po wielu próbach) jest to intuicyjne, bo czego innego mógłbym oczekiwać i dlaczego. Stąd też nie uważam, że obecne rozwiązanie jest takie, że "kod ten może wpaść w ręce, w które nie powinien." Co masz na myśli zrezur? Ten kod wykonywany jest *po* wykonaniu naszego zadania, więc to nie zadanie, ale onAdvance widzi możliwe "śmieci", np. ThreadLocal.

    OdpowiedzUsuń
  7. Jacku,

    podczas prezentacji na JUGu padło stwierdzenie, że wejście przez ten sam wątek w sekcję krytyczną chronioną tym samym monitorem po raz drugi, to typowy NOOP. Na potwierdzenie moich słów, że nie do końca tak jest, i że intrinsic lock jest w Javie zaimplementowany jako klasyczny reentrant lock - kawałek oficjalnej dokumentacji:
    http://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html
    Dlaczego intrinsic lock posiada charakterystykę reentrant locka oraz kilka innych istotnych szczegółów, o których zapomina większość Javowców, chętnie poopowiadam w przyszłości na jakimś JUGu :)
    Pozdr.

    OdpowiedzUsuń
  8. Trzeba przyznać, że moja niewiedza poszybowała już po pierwszym Twoim zdaniu. Gdybyś zechciał oświecić mnie, co miałeś na myśli, podczas jakiegoś JUGa, byłbym wniebowzięty. Przyjmujesz zaproszenie?

    OdpowiedzUsuń