2013-12-02 1 views
2

Я создаю GUI в JavaFX для довольно большого Java-проекта. В этом проекте много разных рабочих потоков, выполняющих тяжелые вычисления в фоновом режиме, и я пытаюсь визуализировать ход этих рабочих потоков в графическом интерфейсе. По прогресса я имею в виду не только голый процент, но и другие переменные, не содержащиеся в классе задач, таких, как (например):Проблемы параллелизма в утилите JavaFX GUI Updater

  • Текущий файл
  • подсчитывать Текущая ошибка
  • Количество прочитанных байтов до сих пор
  • ...

в этом переменном прогрессе меняется очень быстро и потому, что я должен делать обновления GUI из потока JavaFX (Platform.runLater()), очередь событий JavaFX перегружена очень д uickly. Я пытаюсь исправить это, создав класс утилиты, способный асинхронно обновлять свойства GUI из-за пределов потоков JavaFX. Быстрое последовательное обновление должно быть пропущено, чтобы отображать только последнее значение и, таким образом, избегать поворота очереди событий JavaFX с помощью Runnables.

Поэтому я построил следующий класс GUIUpdater, чтобы связать свойства (обычно элемент GUI, такой как метка) с ObservableValues ​​(например, SimpleStringProperty). Этот класс имеет два InnerClasses:

  • PropertyUpdater отвечает за связывание одного свойства к одному ObservableValue и его обновления.
  • Updater предоставляет повторно используемый объект Runnable для Platform.runLater().

Класс утилиты:

package main; 

import java.util.concurrent.ConcurrentLinkedQueue; 

import javafx.application.Platform; 
import javafx.beans.property.Property; 
import javafx.beans.value.ChangeListener; 
import javafx.beans.value.ObservableValue; 

/** 
* Class for enabling fast updates of GUI components from outside the JavaFX thread. 
* Updating GUI components (such as labels) should be done from the JavaFX thread by using Platform.runLater for example. 
* This makes it hard to update the GUI with a fast changing variable as it is very easy to fill up the JavaFX event queue faster than it can be emptied (i.e. faster than it can be drawn). 
* This class binds ObservableValues to (GUI) Properties and ensures that quick consecutive updates are ignored, only updating to the latest value. 
*/ 
public class GUIUpdater { 
    private ConcurrentLinkedQueue<PropertyUpdater<?>> dirtyPropertyUpdaters = new ConcurrentLinkedQueue<>(); 
    private Updater          updater     = new Updater(); 
    private boolean          isUpdating    = false; 

    /** 
    * Binds an ObservableValue to a Property. 
    * Updates to the ObservableValue can be made from outside the JavaFX thread and the latest update will be reflected in the Property. 
    * @param property  (GUI) Property to be updated/ 
    * @param observable ObservableValue to update the GUI property to. 
    */ 
    public <T> void bind(Property<T> property, ObservableValue<T> observable) { 
     PropertyUpdater<T> propertyUpdater = new PropertyUpdater<>(property, observable); 
     observable.addListener(propertyUpdater); 
    } 

    /** 
    * Unbinds the given ObservableValue from the given Property. 
    * Updates to the ObservableValue will no longer be reflected in the Property. 
    * @param property  (GUI) Property to unbind the ObservableValue from. 
    * @param observable ObservableValue to unbind from the given Property. 
    */ 
    public <T> void unbind(Property<T> property, ObservableValue<T> observable) { 
     PropertyUpdater<T> tmpPropertyUpdater = new PropertyUpdater<>(property, observable); 
     observable.removeListener(tmpPropertyUpdater); 
    } 

    /** 
    * Schedules an update to the GUI by using a call to Platform.runLater(). 
    * The updated property is added to the dirtyProperties list, marking it for the next update round. 
    * Will only submit the event to the event queue if the event isn't in the event queue yet. 
    * @param updater 
    */ 
    private void scheduleUpdate(PropertyUpdater<?> updater) { 
     this.dirtyPropertyUpdaters.add(updater); 

     // Make sure the isUpdating var isn't changed concurrently by the Updater thread (on the JavaFX event queue) 
     synchronized (this) { 
      if (!this.isUpdating) { 
       this.isUpdating = true; 
       Platform.runLater(this.updater); 
      } 
     } 
    } 

    /** 
    * Class used for binding a single ObservableValue to a Property and updating it. 
    * 
    * @param <T> 
    */ 
    private class PropertyUpdater<T> implements ChangeListener<T> { 
     private boolean    isDirty  = false; 
     private Property<T>   property = null; 
     private ObservableValue<T> observable = null; 

     public PropertyUpdater(Property<T> property, ObservableValue<T> observable) { 
      this.property = property; 
      this.observable = observable; 
     } 

     @Override 
     /** 
     * Called whenever the ObservableValue has changed. Marks this Updater as dirty. 
     */ 
     public synchronized void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) { 
      if (!this.isDirty) { 
       this.isDirty = true; 
       GUIUpdater.this.scheduleUpdate(this); 
      } 
     } 

     /** 
     * Updates the Property to the ObservableValue and marks it as clean again. 
     * Should only be called from the JavaFX thread. 
     */ 
     public synchronized void update() { 
      T value = this.observable.getValue(); 
      this.property.setValue(value); 
      this.isDirty = false; 
     } 

     @Override 
     /** 
     * Two PropertyUpdaters are equals if their Property and ObservableValue map to the same object (address). 
     */ 
     public boolean equals(Object otherObj) { 
      PropertyUpdater<?> otherUpdater = (PropertyUpdater<?>) otherObj; 
      if (otherObj == null) { 
       return false; 
      } else { 
       // Only compare addresses (comparing with equals also compares contents): 
       return (this.property == otherUpdater.property) && (this.observable == otherUpdater.observable); 
      } 
     } 
    } 

    /** 
    * Simple class containing the Runnable for the call to Platform.runLater. 
    * Hence, the run() method should only be called from the JavaFX thread. 
    * 
    */ 
    private class Updater implements Runnable { 

     @Override 
     public void run() { 
      // Loop through the individual PropertyUpdaters, updating them one by one: 
      while(!GUIUpdater.this.dirtyPropertyUpdaters.isEmpty()) { 
       PropertyUpdater<?> curUpdater = GUIUpdater.this.dirtyPropertyUpdaters.poll(); 
       curUpdater.update(); 
      } 

      // Make sure we're not clearing the mark when scheduleUpdate() is still setting it: 
      synchronized (GUIUpdater.this) { 
       GUIUpdater.this.isUpdating = false; 
      } 
     } 

    } 
} 

И это простой класс для тестирования класса GUIUpdater утилиты:

package main; 

import javafx.application.Application; 
import javafx.beans.property.SimpleStringProperty; 
import javafx.concurrent.Task; 
import javafx.event.ActionEvent; 
import javafx.event.EventHandler; 
import javafx.scene.Scene; 
import javafx.scene.control.Button; 
import javafx.scene.control.Label; 
import javafx.scene.control.ProgressBar; 
import javafx.scene.layout.FlowPane; 
import javafx.stage.Stage; 

public class JavaFXTest extends Application { 
    private GUIUpdater guiUpdater = new GUIUpdater(); 
    private Label  lblState = new Label(); 
    private ProgressBar prgProgress = new ProgressBar(); 

    public static void main(String args[]) { 
     JavaFXTest.launch(args); 
    } 

    @Override 
    public void start(Stage primaryStage) throws Exception { 
     // Init window: 
     FlowPane flowPane = new FlowPane(); 
     primaryStage.setScene(new Scene(flowPane)); 
     primaryStage.setTitle("JavaFXTest"); 

     // Add a Label and a progressBar: 
     flowPane.getChildren().add(this.lblState); 
     flowPane.getChildren().add(this.prgProgress); 

     // Add button: 
     Button btnStart = new Button("Start"); 
     btnStart.setOnAction(new EventHandler<ActionEvent>() { 

      @Override 
      public void handle(ActionEvent event) { 
       // Create task: 
       TestTask testTask = new TestTask(); 

       // Bind: 
       JavaFXTest.this.guiUpdater.bind(JavaFXTest.this.lblState.textProperty(), testTask.myStateProperty()); 
       JavaFXTest.this.prgProgress.progressProperty().bind(testTask.progressProperty()); // No need to use GUIUpdater here, Task class provides the same functionality for progress. 

       // Start task: 
       Thread tmpThread = new Thread(testTask); 
       tmpThread.start(); 
      } 
     }); 
     flowPane.getChildren().add(btnStart); 

     // Show: 
     primaryStage.show(); 
    } 

    /** 
    * A simple task containing a for loop to simulate a fast running and fast updating process. 
    * @author DePhille 
    * 
    */ 
    private class TestTask extends Task<Void> { 
     private SimpleStringProperty myState = new SimpleStringProperty(); 

     @Override 
     protected Void call() throws Exception { 

      // Count: 
      try { 
       int maxValue = 1000000; 

       System.out.println("Starting..."); 
       for(int i = 0; i < maxValue; i++) { 
        this.updateProgress(i, maxValue - 1); 
        this.myState.set("Hello " + i); 
       } 
       System.out.println("Done!");  
      } catch(Exception e) { 
       e.printStackTrace(); 
      } 

      // Unbind: 
      JavaFXTest.this.guiUpdater.unbind(JavaFXTest.this.lblState.textProperty(), this.myStateProperty()); 
      return null; 
     } 

     public SimpleStringProperty myStateProperty() { 
      return this.myState; 
     } 

    } 

} 

Проблемы с кодом, что иногда Ярлык не обновляется до последнего значения (в данном случае 999999). Похоже, что это происходит сразу после запуска приложения, поэтому запуск приложения, нажатие кнопки «Пуск», закрытие и повторение этого процесса должны повторить проблему после нескольких попыток. Насколько я вижу, я добавил synchronized блоков, где это необходимо, поэтому я не понимаю, откуда эта проблема.

Несмотря на то, что я в первую очередь ищу решение описанной проблемы, все предложения очень ценятся (даже те, которые не связаны с проблемой)! Я добавил комментарии и в код, поэтому я надеюсь, что вместе с вышеизложенной информацией вы получите достаточно подробных сведений о проблеме и коде.

Заранее благодарен!

ответ

0

Я был в состоянии решить проблему самостоятельно. Через несколько дней после добавления System.out в разных местах выяснилось, что проблема связана с проблемами параллелизма с переменной isUpdating.Проблема возникла, когда поток JavaFX находился между циклами while и блоком synchronized в Updater.run. Я решил проблему, сделав как методы Updater.run, так и GUIUpdater.scheduleUpdate синхронизированными на одном и том же объекте.

Я также сделал GUIUpdater в статический только объект как имеющие несколько экземпляров будут размещать Runnables в очереди событий JavaFX независимо от других GUIUpdater случаев, засоряя очередь событий. В общем, это результирующий GUIUpdater класс:

package be.pbeckers.javafxguiupdater; 

import java.util.concurrent.ConcurrentLinkedQueue; 

import javafx.application.Platform; 
import javafx.beans.property.Property; 
import javafx.beans.value.ChangeListener; 
import javafx.beans.value.ObservableValue; 


/** 
* Class for enabling fast updates of GUI components from outside the JavaFX thread. 
* Updating GUI components (such as labels) should be done from the JavaFX thread by using Platform.runLater for example. 
* This makes it hard to update the GUI with a fast changing variable as it is very easy to fill up the JavaFX event queue faster than it can be emptied (i.e. faster than it can be drawn). 
* This class binds ObservableValues to (GUI) Properties and ensures that quick consecutive updates are ignored, only updating to the latest value. 
*/ 
public abstract class GUIUpdater { 
    private static ConcurrentLinkedQueue<PropertyUpdater<?>> dirtyPropertyUpdaters = new ConcurrentLinkedQueue<>(); 
    private static Updater          updater     = new Updater(); 
    private static boolean          isUpdating    = false; 

    /** 
    * Binds an ObservableValue to a Property. 
    * Updates to the ObservableValue can be made from outside the JavaFX thread and the latest update will be reflected in the Property. 
    * @param property  (GUI) Property to be updated/ 
    * @param observable ObservableValue to update the GUI property to. 
    */ 
    public static <T> void bind(Property<T> property, ObservableValue<T> observable) { 
     PropertyUpdater<T> propertyUpdater = new PropertyUpdater<>(property, observable); 
     observable.addListener(propertyUpdater); 
    } 

    /** 
    * Unbinds the given ObservableValue from the given Property. 
    * Updates to the ObservableValue will no longer be reflected in the Property. 
    * @param property  (GUI) Property to unbind the ObservableValue from. 
    * @param observable ObservableValue to unbind from the given Property. 
    */ 
    public static <T> void unbind(Property<T> property, ObservableValue<T> observable) { 
     PropertyUpdater<T> tmpPropertyUpdater = new PropertyUpdater<>(property, observable); 
     observable.removeListener(tmpPropertyUpdater); 
    } 

    /** 
    * Schedules an update to the GUI by using a call to Platform.runLater(). 
    * The updated property is added to the dirtyProperties list, marking it for the next update round. 
    * Will only submit the event to the event queue if the event isn't in the event queue yet. 
    * @param updater 
    */ 
    private static synchronized void scheduleUpdate(PropertyUpdater<?> updater) { 
     GUIUpdater.dirtyPropertyUpdaters.add(updater); 

     if (!GUIUpdater.isUpdating) { 
      GUIUpdater.isUpdating = true; 
      Platform.runLater(GUIUpdater.updater); 
     } 
    } 

    /** 
    * Class used for binding a single ObservableValue to a Property and updating it. 
    * 
    * @param <T> 
    */ 
    private static class PropertyUpdater<T> implements ChangeListener<T> { 
     private boolean    isDirty  = false; 
     private Property<T>   property = null; 
     private ObservableValue<T> observable = null; 

     public PropertyUpdater(Property<T> property, ObservableValue<T> observable) { 
      this.property = property; 
      this.observable = observable; 
     } 

     @Override 
     /** 
     * Called whenever the ObservableValue has changed. Marks this Updater as dirty. 
     */ 
     public synchronized void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) { 
      if (!this.isDirty) { 
       this.isDirty = true; 
       GUIUpdater.scheduleUpdate(this); 
      } 
     } 

     /** 
     * Updates the Property to the ObservableValue and marks it as clean again. 
     * Should only be called from the JavaFX thread. 
     */ 
     public synchronized void update() { 
      T value = this.observable.getValue(); 
      this.property.setValue(value); 
      this.isDirty = false; 
     } 

     @Override 
     /** 
     * Two PropertyUpdaters are equals if their Property and ObservableValue map to the same object (address). 
     */ 
     public boolean equals(Object otherObj) { 
      PropertyUpdater<?> otherUpdater = (PropertyUpdater<?>) otherObj; 
      if (otherObj == null) { 
       return false; 
      } else { 
       // Only compare addresses (comparing with equals also compares contents): 
       return (this.property == otherUpdater.property) && (this.observable == otherUpdater.observable); 
      } 
     } 
    } 

    /** 
    * Simple class containing the Runnable for the call to Platform.runLater. 
    * Hence, the run() method should only be called from the JavaFX thread. 
    * 
    */ 
    private static class Updater implements Runnable { 

     @Override 
     public void run() { 
      synchronized (GUIUpdater.class) { 

       // Loop through the individual PropertyUpdaters, updating them one by one: 
       while(!GUIUpdater.dirtyPropertyUpdaters.isEmpty()) { 
        PropertyUpdater<?> curUpdater = GUIUpdater.dirtyPropertyUpdaters.poll(); 
        curUpdater.update(); 
       } 

       // Mark as updated: 
       GUIUpdater.isUpdating = false;    
      } 
     } 

    } 
} 

И это немного обновленная версия класса тестера (я не вдаваясь в подробности на этом, как это совершенно неважно):

package be.pbeckers.javafxguiupdater.test; 

import be.pbeckers.javafxguiupdater.GUIUpdater; 
import javafx.application.Application; 
import javafx.beans.property.SimpleStringProperty; 
import javafx.concurrent.Task; 
import javafx.event.ActionEvent; 
import javafx.event.EventHandler; 
import javafx.scene.Scene; 
import javafx.scene.control.Button; 
import javafx.scene.control.Label; 
import javafx.scene.control.ProgressBar; 
import javafx.scene.layout.FlowPane; 
import javafx.stage.Stage; 

public class JavaFXTest extends Application { 
    private Label  lblCurFile  = new Label(); 
    private Label  lblErrors  = new Label(); 
    private Label  lblBytesParsed = new Label(); 
    private ProgressBar prgProgress  = new ProgressBar(); 

    public static void main(String args[]) { 
     JavaFXTest.launch(args); 
    } 

    @Override 
    public void start(Stage primaryStage) throws Exception { 
     // Init window: 
     FlowPane flowPane = new FlowPane(); 
     primaryStage.setScene(new Scene(flowPane)); 
     primaryStage.setTitle("JavaFXTest"); 

     // Add a few Labels and a progressBar: 
     flowPane.getChildren().add(this.lblCurFile); 
     flowPane.getChildren().add(this.lblErrors); 
     flowPane.getChildren().add(this.lblBytesParsed); 
     flowPane.getChildren().add(this.prgProgress); 

     // Add button: 
     Button btnStart = new Button("Start"); 
     btnStart.setOnAction(new EventHandler<ActionEvent>() { 

      @Override 
      public void handle(ActionEvent event) { 
       // Create task: 
       TestTask testTask = new TestTask(); 

       // Bind: 
       GUIUpdater.bind(JavaFXTest.this.lblCurFile.textProperty(), testTask.curFileProperty()); 
       GUIUpdater.bind(JavaFXTest.this.lblErrors.textProperty(), testTask.errorsProperty()); 
       GUIUpdater.bind(JavaFXTest.this.lblBytesParsed.textProperty(), testTask.bytesParsedProperty()); 
       JavaFXTest.this.prgProgress.progressProperty().bind(testTask.progressProperty()); // No need to use GUIUpdater here, Task class provides the same functionality for progress. 

       // Start task: 
       Thread tmpThread = new Thread(testTask); 
       tmpThread.start(); 
      } 
     }); 
     flowPane.getChildren().add(btnStart); 

     // Show: 
     primaryStage.show(); 
    } 

    /** 
    * A simple task containing a for loop to simulate a fast running and fast updating process. 
    * @author DePhille 
    * 
    */ 
    private class TestTask extends Task<Void> { 
     private SimpleStringProperty curFile  = new SimpleStringProperty(); 
     private SimpleStringProperty errors  = new SimpleStringProperty(); 
     private SimpleStringProperty bytesParsed = new SimpleStringProperty(); 

     @Override 
     protected Void call() throws Exception { 

      // Count: 
      try { 
       int maxValue = 1000000; 
       long startTime = System.currentTimeMillis(); 

       System.out.println("Starting..."); 
       for(int i = 0; i < maxValue; i++) { 
        this.updateProgress(i, maxValue - 1); 

        // Simulate some progress variables: 
        this.curFile.set("File_" + i + ".txt"); 
        if ((i % 1000) == 0) { 
         //this.errors.set("" + (i/1000) + " Errors"); 
        } 
        //this.bytesParsed.set("" + (i/1024) + " KBytes"); 
       } 
       long stopTime = System.currentTimeMillis(); 
       System.out.println("Done in " + (stopTime - startTime) + " msec!"); 
      } catch(Exception e) { 
       e.printStackTrace(); 
      } 

      // Unbind: 
      GUIUpdater.unbind(JavaFXTest.this.lblCurFile.textProperty(), this.curFileProperty()); 
      GUIUpdater.unbind(JavaFXTest.this.lblErrors.textProperty(), this.errorsProperty()); 
      GUIUpdater.unbind(JavaFXTest.this.lblBytesParsed.textProperty(), this.bytesParsedProperty()); 
      return null; 
     } 

     public SimpleStringProperty curFileProperty() { 
      return this.curFile; 
     } 

     public SimpleStringProperty errorsProperty() { 
      return this.errors; 
     } 

     public SimpleStringProperty bytesParsedProperty() { 
      return this.bytesParsed; 
     } 

    } 

} 
0

Я считаю, что эта функциональность может быть достигнута через Task «s messageProperty:

public void handle(ActionEvent event) { 
    ... 
    JavaFXTest.this.lblState.textProperty().bind(testTask.messageProperty()); 
    ... 
} 

... 

protected Void call() throws Exception { 
    ... 
    this.updateProgress(i, maxValue - 1); 
    this.updateMessage("Hello " + i); 
    ... 
} 
+0

Это действительно работает для одной строки (например, ProgressProperty). Однако мне нужно повторить это поведение для нескольких свойств, а не только для тех, которые предоставляются классом Task. Теперь я понимаю, что это может быть не слишком ясно из первоначального вопроса, поэтому я также обновил этот вопрос. – DePhille

Смежные вопросы