9 Replies Latest reply: Nov 15, 2011 11:33 AM by jsmith RSS

    Multithreading

    Knut Arne Vedaa
      I have an application which performs a long-running computation. The obvious thing to do is move this into a different thread. However I'm not quite sure how to do that in JavaFX, specifically how the threads can interact.

      1) When I run the computation on the application thread, if I set the text of a label to e.g. "Starting computation..." before I start the computation, the label will not be updated before after the computation has finished. If the rendering is done in a seperate thread from the application thread, why doesn't it update the UI immediately?

      2) How can I use the concurrent features of JavaFX (Service, Worker, Task) to perform the computation in a background thread?

      3) How can I update the UI from the background thread (e.g. updating a progress bar)? The doc says:
      JavaFX application thread: This is the primary thread used by JavaFX application developers. Any “live” scene, which is a scene that is part of a window, must be accessed from this thread.
      What's the meaning of "access" here? Does it mean that I cannot call any methods on UI elements from other threads than the application thread?
        • 1. Re: Multithreading
          MiPa
          Maybe this article is helpfull for you:
          http://fxexperience.com/2011/07/worker-threading-in-javafx-2-0/
          • 2. Re: Multithreading
            Knut Arne Vedaa
            Very interesting, I'll take a look and get back here if I'm still lost. :)
            • 3. Re: Multithreading
              Knut Arne Vedaa
              Ok, so I gather that I should do something along the lines of:
              Label statusLabel = ...
              Button runButton = ...
              ListView<Person> personsView = ...
              
              Task task = new Task<ObservableList<Person>>() {
              @Override
                protected ObservableList<Person> call() {
                   updateMessage("Computing persons...");
                   List<Person> persons = computePersons();
                   updateMessage("Finished.");
                   return new ObservableListWrapper<Person>(persons);
              };
              
              statusLabel.textProperty().bind(task.messageProperty());
              runButton.disableProperty().bind(task.runningProperty());
              personsView.itemsProperty().bind(task.valueProperty());
                      
              new Thread(task).start();
              This works fine.

              However, I still have questions.

              1) I'm still not sure what it means to "access" the scene graph from a worker thread.

              Can I read properties of "live" nodes from a worker thread? (I did, and I could.)

              Can I modify properties of "live" nodes from a worker thread? Don't tell anybody, but I did, and I could. This little trick, for instance (in Task):
              @Override
              protected void done() {
                super.done();
                runButton.setText("Voila!");
              }            
              Works fine. However, if I try:
              @Override
              protected void done() {
                super.done();
                toolbar.getChildren().add(new Button("hello"));              
              Nothing happens. (Nothing bad, and nothing good. It didn't get ugly either, actually.)

              From one of the comments on the blog post though, it would seem that this kind of thing should generate an exception:

              "Actually, what we want to do is to “fail-fast” much like the Collection’s framework or WPF or SWT, such that if you attempt to use the scene graph from the wrong thread we throw an unchecked exception, as opposed to Swing which would just carry on (and perhaps deadlock, or perhaps silently corrupt data, or perhaps just work)."

              2) What is the recommended approach for a general callback function at task completion if overriding "done" as shown above is not recommended? It seems that it would work to listen to either runningProperty and stateProperty. But wouldn't a "doneProperty" make sense as well, if you just cared about whether it had finished or not?

              3) I'm not quite sure when to use Service instead of Task. It seems that Service is just some wrapper around Task?
              • 4. Re: Multithreading
                jsmith
                1) I'm still not sure what it means to "access" the scene graph from a worker thread.
                Can I read properties of "live" nodes from a worker thread? (I did, and I could.)
                Can I modify properties of "live" nodes from a worker thread? Don't tell anybody, but I did, and I could.
                ...
                From one of the comments on the blog post though, it would seem that this kind of thing should generate an exception
                Access in this case means to either read or write the scene graph data. Don't do either from a worker thread. OK, it is possible to perform these kinds of accesses and manipulations (in that the compiler will not prevent it and sometimes the JavaFX runtime won't throw an exception), but the behaviour could be unpredictable and error prone. So (in my best Father of Nemo voice), "you think you can do these things, but you cannot". I'd suggest passing only immutable data to the worker thread, and only retrieving data from the worker thread using the properties provided by the Service and Task APIs.

                For the done method sample you posted, instead register a listener to the Task's stateProperty and act on that - the JavaFX runtime will take care of ensuring that the listener gets called on the JavaFX Application thread. Something like:
                task.stateProperty().addListener(new ChangeListener<Worker.State>() {
                  @Override public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldState, Worker.State newState) {
                    if (newState = Worker.State.SUCEEDED) {
                       runButton.setText("Voila!");
                       toolbar.getChildren().add(new Button("hello"));
                    }
                  }
                });
                What is the recommended approach for a general callback function at task completion if overriding "done" as shown above is not recommended? It seems that it would work to listen to either runningProperty and stateProperty. But wouldn't a "doneProperty" make sense as well, if you just cared about whether it had finished or not?
                I recall somebody else brought that up somewhere else too. I think Richard's response was that he was trying on the initial phase to keep the API minimal (kind of a less is more philosophy), but was open to adding extra stuff like this if enough people started requesting it after release. You can request such things by filing Jira feature requests.
                I'm not quite sure when to use Service instead of Task. It seems that Service is just some wrapper around Task?
                A Task can only be run once, it is a single execution by a single thread, if you try to run it more than once, it won't do anything on subsequent executions.
                A Service has an abstract method createTask, you must override it when you implement a Service. A Service has the ability to attach an execution pool of potentially multiple threads to it. The Service can be reset, cancelled and re-executed.

                Once thing that it would seem you would be able to do with a Service, but I'm not sure on the semantics of, is running multiple concurrent tasks given a single Service instance. But perhaps you just don't do this and instead spawn a new Service instance for each concurrent task you want to run and attach them all to the same Execution Thread Pool.

                I have some sample code for doing similar things using both Services and Tasks I could post here if you wanted it to compare their usage (perhaps 100 lines of code total).
                • 5. Re: Multithreading
                  875756
                  Nice summary from jsmith.
                  What is the recommended approach for a general callback function at task completion if overriding "done" as shown above is not recommended? It seems that it would work to listen to either runningProperty and stateProperty. But wouldn't a "doneProperty" make sense as well, if you just cared about whether it had finished or not?
                  I recall somebody else brought that up somewhere else too. I think Richard's response was that he was trying on the initial phase to keep the API minimal (kind of a less is more philosophy), but was open to adding extra stuff like this if enough people started requesting it after release. You can request such things by filing Jira feature requests.
                  http://javafx-jira.kenai.com/browse/RT-17449

                  Up vote if you want it, add comments if you have better ideas. And yea, Richard mentioned he was keeping it light until he got more feedback - now's your chance :)
                  • 6. Re: Multithreading
                    Knut Arne Vedaa
                    "you think you can do these things, but you cannot".
                    Good to know. :)
                    I have some sample code for doing similar things using both Services and Tasks I could post here if you wanted it to compare their usage (perhaps 100 lines of code total).
                    Please do.
                    • 7. Re: Multithreading
                      Knut Arne Vedaa
                      http://javafx-jira.kenai.com/browse/RT-17449

                      Up vote if you want it,
                      Already did. :)
                      • 8. Re: Multithreading
                        jsmith
                        Hello Knut,

                        Here is a Task Example. The code will simulate the task of finding friends, which only takes a couple of seconds :-)
                        We add a progress monitor to monitor the progress of the friend search.
                        Note that the task is created in the Run button's action handler, so a new task is created for each run. If we just created the task outside in the body of the start method, it have only worked the first time and after that we would not be able to find our friends again . . . which would be a disappointment.
                        import javafx.application.Application;
                        import javafx.beans.value.*;
                        import javafx.collections.*;
                        import javafx.concurrent.*;
                        import javafx.event.*;
                        import javafx.scene.Scene;
                        import javafx.scene.control.*;
                        import javafx.scene.layout.*;
                        import javafx.stage.Stage;
                        
                        public class TaskTest extends Application {
                          public static void main(String[] args) throws Exception { launch(args); }
                          public void start(final Stage stage) throws Exception {
                            final Label statusLabel = new Label("Status");
                            final Button runButton = new Button("Run");
                            final ListView<String> peopleView = new ListView<String>();
                            peopleView.setPrefSize(220, 162);
                            final ProgressBar progressBar = new ProgressBar();
                            progressBar.prefWidthProperty().bind(peopleView.prefWidthProperty());
                        
                            runButton.setOnAction(new EventHandler<ActionEvent>() {
                              @Override public void handle(ActionEvent actionEvent) {
                                final Task task = new Task<ObservableList<String>>() {
                                  @Override protected ObservableList<String> call() throws InterruptedException {
                                    updateMessage("Finding friends . . .");
                                    for (int i = 0; i < 10; i++) {
                                      Thread.sleep(200);
                                      updateProgress(i+1, 10);
                                    }
                                    updateMessage("Finished.");
                                    return FXCollections.observableArrayList("John", "Jim", "Geoff", "Jill", "Suki");
                                  }
                        //          @Override protected void done() {
                        //            super.done();
                        //            System.out.println("This is bad, do not do this, this thread " + Thread.currentThread() + " is not the FXApplication thread.");
                        //            runButton.setText("Voila!");
                        //          }
                                };
                        
                                statusLabel.textProperty().bind(task.messageProperty());
                                runButton.disableProperty().bind(task.runningProperty());
                                peopleView.itemsProperty().bind(task.valueProperty());
                                progressBar.progressProperty().bind(task.progressProperty());
                                task.stateProperty().addListener(new ChangeListener<Worker.State>() {
                                  @Override public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldState, Worker.State newState) {
                                    if (newState == Worker.State.SUCCEEDED) {
                                      System.out.println("This is ok, this thread " + Thread.currentThread() + " is the JavaFX Application thread.");
                                      runButton.setText("Voila!");
                                    }
                                  }
                                });
                        
                                new Thread(task).start();
                              }
                            });
                        
                            final VBox layout =
                              VBoxBuilder.create().spacing(8).children(
                                VBoxBuilder.create().spacing(5).children(
                                  HBoxBuilder.create().spacing(10).children(
                                    runButton,
                                    statusLabel).build(),
                                  progressBar
                                ).build(),
                                peopleView
                              ).build();
                            layout.setStyle("-fx-background-color: cornsilk; -fx-padding:10; -fx-font-size: 16;");
                            Scene scene = new Scene(layout);
                            stage.setScene(scene);
                            stage.show();
                          }
                        }
                        • 9. Re: Multithreading
                          jsmith
                          Here is another example of doing a similar Friend lookup, but this time using a Service instead of a Task.
                          Note how the Service can be created in the start method rather than in the action handler for the Run button.
                          This version adds a few more features to the previous basic Task based version, like a spinning animation so it can be seen that the main rendering thread is unaffected by the work being done in the Service thread, and the ability to mouse over the spinning animation and cancel an execution of the Service. The main reason for adding these features is to demonstrate how you can use the Worker's State and Running properties to perform actions as the Worker transfers between different states.
                          If you just want a straight comparison between the Service code and Task code you can strip out the spinning animation. After that you will see that they are pretty similar (mainly because all of the core APIs are in Worker from which both Task and Service inherit).
                          import javafx.animation.*;
                          import javafx.application.Application;
                          import javafx.beans.property.SimpleDoubleProperty;
                          import javafx.beans.value.ChangeListener;
                          import javafx.beans.value.ObservableValue;
                          import javafx.collections.*;
                          import javafx.concurrent.*;
                          import javafx.event.*;
                          import javafx.geometry.*;
                          import javafx.scene.Scene;
                          import javafx.scene.control.*;
                          import javafx.scene.effect.*;
                          import javafx.scene.input.MouseEvent;
                          import javafx.scene.layout.*;
                          import javafx.scene.paint.Color;
                          import javafx.scene.shape.Rectangle;
                          import javafx.stage.Stage;
                          import javafx.util.Duration;
                          
                          public class ServiceTest extends Application {
                            public static void main(String[] args) throws Exception { launch(args); }
                            public void start(final Stage stage) throws Exception {
                              // set up some controls.
                              final Label statusLabel = new Label();
                              final Label celebrateLabel = new Label();
                              final Button searchButton = new Button("Search");
                              searchButton.setTooltip(new Tooltip("Look for friends"));
                              final ListView<String> peopleView = new ListView<String>();
                              peopleView.setMaxHeight(Double.MAX_VALUE);
                              peopleView.setTooltip(new Tooltip("Friends we find are displayed here"));
                              final ProgressBar progressBar = new ProgressBar();
                              progressBar.prefWidthProperty().bind(peopleView.widthProperty());
                          
                              // create a service.
                              final Service friendFinder = new Service<ObservableList<String>>() {
                                @Override protected Task createTask() {
                                  return new Task<ObservableList<String>>() {
                                    @Override protected ObservableList<String> call() throws InterruptedException {
                                      updateMessage("Finding friends . . .");
                                      updateProgress(0, 10);
                                      for (int i = 0; i < 10; i++) {
                                        Thread.sleep(300);
                                        updateProgress(i + 1, 10);
                                      }
                                      updateMessage("Found them.");
                                      return FXCollections.observableArrayList("John", "Jim", "Geoff", "Jill", "Suki", "Chiang", "Lin");
                                    }
                                  };
                                }
                              };
                          
                              // bind interesting service properties to the controls.
                              statusLabel.textProperty().bind(friendFinder.messageProperty());
                              searchButton.disableProperty().bind(friendFinder.runningProperty());
                              peopleView.itemsProperty().bind(friendFinder.valueProperty());
                              progressBar.progressProperty().bind(friendFinder.progressProperty());
                              progressBar.visibleProperty().bind(friendFinder.progressProperty().isNotEqualTo(new SimpleDoubleProperty(ProgressBar.INDETERMINATE_PROGRESS)));
                          
                              // kick off the service on an action.
                              searchButton.setOnAction(new EventHandler<ActionEvent>() {
                                @Override public void handle(ActionEvent actionEvent) {
                                  if (!friendFinder.isRunning()) {
                                    friendFinder.reset();
                                    friendFinder.start();
                                  }
                                }
                              });
                          
                              // create a distracting square with different colors for different states.
                              final Color normalDistractorColor = Color.BURLYWOOD;
                              final Color runningDistractorColor = Color.DARKGREEN;
                              final Color highlightedDistractorColor = Color.FIREBRICK;
                              final Rectangle distractor = new Rectangle(25, 25, normalDistractorColor);
                              distractor.setUserData(false); // maintains whether or not the mouse is in the distractor.
                              distractor.setOpacity(0.8);
                              Tooltip.install(distractor, new Tooltip("If you are looking for friends, you can click on me to stop searching"));
                          
                              // rotate the distractor to show that animation and stuff still continues while the work is being done.
                              RotateTransition rt = new RotateTransition(Duration.millis(3000), distractor);
                              rt.setByAngle(360);
                              rt.setCycleCount(Animation.INDEFINITE);
                              rt.setInterpolator(Interpolator.LINEAR);
                              rt.play();
                          
                              // create some effects for the animation.
                              final BoxBlur normal      = new BoxBlur();
                              final BoxBlur highlighted = new BoxBlur(); highlighted.setInput(new DropShadow());
                              distractor.setEffect(normal);
                          
                              // the distracting animation can be used to cancel the friend lookup.
                              distractor.setOnMouseClicked(new EventHandler<MouseEvent>() {
                                @Override public void handle(MouseEvent mouseEvent) {
                                  if (friendFinder.isRunning()) {
                                    friendFinder.cancel();
                                  }
                                }
                              });
                              distractor.setOnMouseEntered(new EventHandler<MouseEvent>() {
                                @Override public void handle(MouseEvent mouseEvent) {
                                  distractor.setUserData(true);
                                  if (friendFinder.isRunning()) {
                                    distractor.setEffect(highlighted);
                                    distractor.setFill(highlightedDistractorColor);
                                  }
                                }
                              });
                              distractor.setOnMouseExited(new EventHandler<MouseEvent>() {
                                @Override public void handle(MouseEvent mouseEvent) {
                                  distractor.setUserData(false);
                                  distractor.setEffect(normal);
                                  distractor.setFill(normalDistractorColor);
                                }
                              });
                          
                              // do something when the service has succeeded.
                              friendFinder.stateProperty().addListener(new ChangeListener<Worker.State>() {
                                @Override public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldState, Worker.State newState) {
                                  switch (newState) {
                                    case SCHEDULED:
                                      celebrateLabel.setVisible(false);
                                      progressBar.progressProperty().bind(friendFinder.progressProperty());  // workaround, we should be able to permanently bind to the progress, but unless we do this sometimes the progress does not always reach the end.
                                      break;
                                    case READY:
                                    case RUNNING:
                                      celebrateLabel.setVisible(false);
                                      break;
                                    case SUCCEEDED:
                                      celebrateLabel.setVisible(true);
                                      celebrateLabel.setText("Let's grab a beer.");
                                      progressBar.progressProperty().unbind();
                                      progressBar.setProgress(1);  // workaround, we should be able to permanently bind to the progress, but unless we do this sometimes the progress does not always reach the end. (even this workaround didn't work, so I have no idea about this...)
                                      break;
                                    case CANCELLED:
                                    case FAILED:
                                      celebrateLabel.setVisible(true);
                                      celebrateLabel.setText("Bummer dude, party's over.");
                                      break;
                                  }
                                }
                              });
                          
                              // maintain the distractor colors while the service is running.
                              friendFinder.runningProperty().addListener(new ChangeListener<Boolean>() {
                                @Override public void changed(ObservableValue<? extends Boolean> observableValue, Boolean aBoolean, Boolean isRunning) {
                                  if (isRunning) {
                                    if (!(Boolean) distractor.getUserData()) {
                                      distractor.setEffect(normal);
                                      distractor.setFill(runningDistractorColor);
                                    } else {
                                      distractor.setEffect(highlighted);
                                      distractor.setFill(highlightedDistractorColor);
                                    }
                                  } else {
                                    distractor.setEffect(normal);
                                    distractor.setFill(normalDistractorColor);
                                  }
                                }
                              });
                          
                              // layout the scene.
                              stage.setTitle("Friend Finder");
                              VBox.setVgrow(peopleView, Priority.ALWAYS);
                              HBox.setMargin(statusLabel, new Insets(3, 0, 0, 0));     // workaround because setting HBox alignment BASELINE_LEFT causes layout glitches.
                              HBox.setMargin(celebrateLabel, new Insets(3, 0, 0, 0));  // workaround because setting HBox alignment BASELINE_LEFT causes layout glitches.
                              HBox.setHgrow(statusLabel, Priority.SOMETIMES);
                              StackPane.setAlignment(distractor, Pos.TOP_RIGHT);
                              final Pane layout =
                                StackPaneBuilder.create().alignment(Pos.TOP_RIGHT).children(
                                  VBoxBuilder.create().spacing(8).children(
                                    VBoxBuilder.create().spacing(5).children(
                                      HBoxBuilder.create().spacing(10).children(
                                        searchButton,
                                        statusLabel,
                                        celebrateLabel
                                      ).build(),
                                      progressBar
                                    ).build(),
                                    peopleView
                                  ).build(),
                                  distractor
                                ).build();
                              layout.setStyle("-fx-background-color: cornsilk; -fx-padding:10; -fx-font-size: 16;");
                              Scene scene = new Scene(layout, 480, 360);
                              stage.setScene(scene);
                              stage.show();
                            }
                          }
                          Edited by: jsmith on Nov 15, 2011 9:27 AM => friendFinder Service Code pending update to make the service interruptible so that it reacts more efficiently to a cancel call.