JavaFX - kolejkowanie wykonywania zadań w wątkach / wstrzymanie wątku

0

Cześć,
Mam problem z kolejkowaniem zadań w JavieFX. Chciałbym, aby moja aplikacja była w pełni (no może prawie w pełni) zautomatyzowana i wymagała minimum interakcji z użytkownikiem. Posiada ona 4 - 5 "stepów" które mają się wykonać po sobie kolejno, ale dopiero jeżeli wykona się poprzedni z nich. Stwierdziłem, że dobrym pomysłem będzie podzielenie tego na osobne wątki i uruchamianie ich kolejno po sobie - dzięki wątkom mam łatwą możliwość restartu "stepu", jeżeli któryś z nich zakończy się niepowodzeniem (np. brak połączenia z internetem i brak możliwości pobrania pliku z serwera). Myślałem że będę tym mógł zarządzać za pomocą zwykłego switcha w prawie nieskończonej pętli - w zależności od stanu zakończenia wątku (succeeded, failed) mam możliwość przejścia dalej lub restartu danego stepu (inkrementując INTa do switcha).

Niestety, tu się zaczynają schody... nie wziąłem pod uwagę, że unlimited-switch będzie mi zawieszał appkę, nawet jeżeli jest robiony w osobnym Tasku :/ Poniżej przedstawiam sposób, w jaki chciałem to zrobić:

MainApp.java

@Override
    public void start(Stage primaryStage) throws Exception {
        locale = new Locale(params.getLanguage());

        this.primaryStage = primaryStage;
        this.primaryStage.setTitle("MyApp");

        try {
            FXMLLoader loader = new FXMLLoader(MyApp.class.getResource("/fxml/RootLayout.fxml"));
            rootLayout = (BorderPane) loader.load();
            Scene scene = new Scene(rootLayout);
            primaryStage.setScene(scene);
            primaryStage.show();
            log.debug("Root frame loaded properly");
        } catch (IOException e) {
            log.error("Failed to load Root Frame properly! " + e);
        }
        showMainOverview();


        Task task = new Task() {
            @Override
            protected Object call() throws Exception {

                do {
                    switch (params.getProgressStep()) {

                        //  connection with server and download document
                        case 0: {
                            try {
                                connController = new ConnController();
                                connController.initialize();
                            } catch (Exception exception) {
                                log.error(exception.getMessage(), exception);
                            }
                            break;
                        }
                        case 1: {
                            showSelectTerminalStage();
                            params.setProgressStep(55);
                            break;
                        }
                        case 55: {
                            break;
                        }
                    }
                 } while (params.getProgressStep() != 99);
                return null;
            }
        };
            Platform.runLater(task);
    }

Oraz ConnController:

private ConnHandlerThread connHandler;
{...}
public void initialize() {
        connHandler = new ConnHandlerThread();
        connHandler.start();
    }

{...}
protected class ConnHandlerThread extends Service<Void> {
//        CountDownLatch

        @Override
        protected Task<Void> createTask() {
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {

                    Connect();
                    CallAndDownload();

                    return null;
                }
            };
        }

        @Override
        protected void succeeded() {
            super.succeeded();
            params.setProgressStep(1);
            log.info("Thread " + this.getClass().getName() + " done.");
            log.debug("Next progress Step: " + params.getProgressStep());
        }

        @Override
        protected void failed() {
            super.failed();
            log.info("Thread " + this.getClass().getName() + " failed.");
        }
        }
    }

Każdy "step" jest zrobiony w bardzo podobny sposób, powycinałem jedynie kod żeby nie był jakiś super długi (całą logikę mam już zaimplementowaną, pozostało jedynie "zakolejkować stepy") - a została tylko idea problemu.
Może nie jest to najbardziej elegancki sposób, ale jedyny jaki mi przyszedł do głowy :/ Niestety nie działa i nie wiem jak sobie mogę z tym poradzić. Wiem, że powinienem zrobić to na lockach - ale nie mam jeszcze dużego doświadczenia w pisaniu aplikacji wielowątkowych - dodatkowo działających w sposób zautomatyzowany. Próbuję to zrobić na CountLatchDown, jednak bez skutku i kończą mi się pomysły... Uruchamianie kolejnych stepów w succeeded() również wolałbym ominąć.

Czy moglibyście mi doradzić, jak mogę obsłużyć "kolejkowanie" zadań? najlepiej, gdyby działało to właśnie w switchu, ale teraz bardzo mocno wątpię czy można to zrobić w ten sposób...

Proszę również o wyrozumiałość - mało dotychczas pisałem appek okienkowych (głównie konsolowe) i dopiero się uczę :)

Pozdrawiam,
Michał :)

1

Skoro chcesz wykonywać jeden po drugim to nie potrzebujesz kolejnych wątków.

Generalnie bez zbytniego mieszania w głowie:

  1. Tworzysz te pięć Tasków.
  2. Pakujesz je do kolekcji zachowującej kolejność dodawania (dowolna lista, LinkedHashSet/Map).
  3. Tworzysz jakąś klasę, która będzie mogła wykonać taki Task. Albo korzystasz z już gotowych jeśli jest.
  4. Odpalasz po kolei.

Np.

 
private MyObserverViewController observer; // jakiś kontroler do widoku

public void doItAll(){
	MyTaskExecutor executor = new MyTaskExecutor();
	List<Task> taskList = new ArrayList<Task>();
	taskList.add(createTask1() /* jakas metoda zwracajaca task nr 1 */ );
	...
	taskList.add(createTaskN());
	
	try {		
		for(Task currentTask: taskList){
			executor.onStart(observer, currentTask); // metoda która powiadomi kontroler widoku o starcie wykonywania taska - np. pojawi się info: "ściągam plik <nazwa>"
			executor.execute(currentTask); // metoda właściwa do wykonywania tasku
			executor.onFinish(observer, currentTask); // metoda, która powiadomi kontroler widoku że jakiś task się udało wykonać, np. info "Ściągnięto plik!"
		}
	} catch (Exception e){
		cleanUpOnFail(); // mówi samo za siebie
	}
}

Oczywiście to, jak to będzie wyglądać zależy od klasy MyTaskExecutor i samych Tasków.

0

Akurat CountDownLatch'e są dobrym rozwiązaniem, aby mieć 100% pewności że taski zostały zakończone. Wait i notyfy ze standardowej obsługi concurrency poszły do lamusa. wait() i await() będą tutaj na miejscu. Po prostu, w miejscu gdzie czekasz na skończenie jednego wątka stawiasz wait(), a po skończeniu zadania robisz await() przed następnym. Ot cała filozofia.

Sam niedawno zacząłem wielowątkowość i korzystałem z tego http://tutorials.jenkov.com/java-concurrency/index.html
Myślę, że jak na początek to wystarczy. Gdybym jednak był w błędzie, to pewnie ktoś mnie za chwilę z niego wyprowadzi.

0

Witam,
Dziękuję bardzo za odpowiedzi - naprawdę wiele mi pomogły. Zaimplementowałem Commanda zgodnie ze wskazówkami usera wartek01 - i bardzo spodobał mi się ten pattern :) jednak nie potrafiłem w nim ustawić poprawnych locków - nie do końca wiedziałem jak ustawić metody onStart i onFinish aby zlockować tylko jeden thread - wykonywały się po sobie, ale współbieżnie do siebie.

Poczytałem trochę na forach i w linku podanym przez panryz i znalazłem jedno rozwiązanie które faktycznie działa - mianowicie ExecutorService pool = Executors.newSingleThreadExecutor(); i wykonuję to aktualnie w taki sposób:

public void doAllTasks() {

//        CommanderController commander = new CommanderController();
        List<Task> taskList = new ArrayList<>();
        ExecutorService pool = Executors.newSingleThreadExecutor();

        taskList.add(ConnController.getConnHandler());
        taskList.add(CardController.getCardThread());

        try {
            for (Task currentService : taskList) {
                pool.submit(currentService);
            }
        } catch (Exception e) {
            log.error(e.getMessage(),e);
        } finally {
            pool.shutdown();
        }
    }

Jak sądzicie, czy warto wykorzystywać newSingleThreadExecutor? macie może z nim związane jakieś doświadczenia? czy jednak lepiej posiedzieć chwilę i zrobić to porządnie z lockami w Commandzie?

0

Witam ponownie,
poradziłem sobie już z kolejkowaniem zadań, ale teraz mam natomiast inny problem :/

Podczas wykonywania jednego z tasków muszę przeprowadzić interakcję z użytkownikiem - użytkownik musi wybrać z listy urządzenie, które program ma później obsługiwać. Dlatego w pewnym momencie wątek musi zostać wstrzymany i oczekiwać aż zostanie naciśnięty przycisk akceptuj - dopiero wtedy wątek może ruszyć dalej. I tutaj pojawia się problem :/ teoretycznie wiem jak powinienem to zrobić, ale nie do końca to działa... mianowicie:

public void pause() {
        synchronized (cardHandler) {
            cardHandler.pause = true;
        }
    }

    public void resume() {
        synchronized (cardHandler) {
            cardHandler.pause = false;
            cardHandler.notify();
        }
    }

protected class CardHandlerThread extends Task<Void> {
        public boolean pause = false;

        @Override
        protected Void call() throws Exception {
            card = new Card();
            try {
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        mainController.handleRepeat();                      //pobiera liste urzadzen
                    }
                });

                    while(card.getCard() == null) {
                        synchronized (this) {
                            if (pause) {
                                wait();
                            }
                        mainController.handleAccept();                      // ponizej
                    }
                    log.info(Card.getCard().getName());
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }

            return null;
        }

oraz metoda handleAccept z MainController:

public void handleAccept() {

        cardController.pause();
        if (!listView.getSelectionModel().isEmpty()) {
            acceptButton.setDisable(false);
        }

        if(acceptButton.isPressed()) {
            card.setCard(listView.getSelectionModel().getSelectedItem());
            cardController.setCard(card);
            cardController.resume();
        }
    }

Problem leży w tym, że nie mogę wznowić tegoż wątku po naciśnięciu przycisku... a gdy wyciągnę cardController.resume(); za if'a to dostaję nieskończoną pętlę - niby działa poprawnie, ale oczekując na wybór urządzenia zżera całego procka...
Macie może pomysł jak powinienem to obsłużyć? ewentualnie czy dopisanie actionPerformeda mogłoby pomóc?

Pozdrawiam :)

0

#łomujborze
Dlaczego nie na Latch ? tylko na wait i notify ?

1 użytkowników online, w tym zalogowanych: 0, gości: 1