在gui线程中触发异步事件

时间:2016-09-21 21:45:55

标签: java events javafx concurrency javafx-8

TL; DR我正在寻找让一个线程在另一个线程中引发事件的方法

编辑:我说“立即”这个词,就像一些评论者指出的那样,是不可能的。我的意思是它应该合理地快速发生,如果gui线程处于空闲状态(如果我的工作正确,应该是),在毫秒到纳秒范围内。(/ p>

案例: 我有一个有Parent类的项目。该Parent类创建一个子线程'Gui',它包含一个javafx应用程序并实现Runnable。 Parent和Gui都引用了相同的BlockingQueue。

我想要发生什么: 我希望能够将对象从父类发送到Gui线程,并让Gui接收某种事件,该事件立即调用某种处理函数,因此我知道从队列中获取一个或多个对象将它们添加到gui。

“观察者模式”的其他解决方案通常涉及一个位于while循环中的观察者,检查一些同步队列中的新数据。这对我的应用程序不起作用,因为Javafx要求仅从gui线程修改gui元素,并且gui线程必须在很大程度上保持空闲状态,以便它有时间重绘事物并响应用户事件。循环会导致应用程序挂起。

我发现似乎有潜力的一个想法是从父线程中断Gui线程,并触发某种事件,但我找不到任何方法来实现这一点。

有什么想法吗?对于这种情况,最佳做法是什么?

2 个答案:

答案 0 :(得分:2)

这里听起来你需要的是通过Platform.runLater(...)调用FX应用程序线程上的UI更新。这将安排一个更新,该更新将在FX应用程序线程有时间后立即执行,只要您没有充斥太多请求,这将非常快。下次渲染脉冲发生时,用户可以看到更新(因此从用户的角度来看,这种情况会尽快发生)。

这是一个最简单的例子:生成数据的异步类直接调度UI上的更新。

首先是一个保存一些数据的简单类。我添加了一些功能来检查" age"数据,即调用构造函数后的时间:

MyDataClass.java

public class MyDataClass {

    private final int value ;

    private final long generationTime ;

    public MyDataClass(int value) {
        this.value = value ;
        this.generationTime = System.nanoTime() ;
    }

    public int getValue() {
        return value ;
    }

    public long age() {
        return System.nanoTime() - generationTime ;
    }
}

这是一个简单的用户界面,可显示收到的所有数据,以及"年龄"数据和所有数据的平均值:

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;

public class UI {

    private final TextArea textArea ;
    private final Parent view ;
    private long total ;
    private long count ;
    private final DoubleProperty average = new SimpleDoubleProperty(0);


    public UI() {
        textArea = new TextArea();

        Label aveLabel = new Label();
        aveLabel.textProperty().bind(average.asString("Average: %.3f"));

        view = new BorderPane(textArea, null, null, aveLabel, null);
    }

    public void registerData(MyDataClass data) {
        textArea.appendText(String.format("Data: %d (received %.3f milliseconds after generation)%n", 
                data.getValue(), data.age()/1_000_000.0)); 
        count++;
        total+=data.getValue();
        average.set(1.0*total / count);
    }

    public Parent getView() {
        return view ;
    }
}

这是一个(异步)睡眠很多并产生随机数据的类(有点像我的实习生......)。目前,它只是对UI的引用,因此它可以直接安排更新:

import java.util.Random;

import javafx.application.Platform;

public class DataProducer extends Thread {

    private final UI ui ;

    public DataProducer(UI ui) {
        this.ui = ui ;
        setDaemon(true);
    }

    @Override
    public void run()  {
        Random rng = new Random();
        try {
            while (true) {
                MyDataClass data = new MyDataClass(rng.nextInt(100));
                Platform.runLater(() -> ui.registerData(data));
                Thread.sleep(rng.nextInt(1000) + 250);
            } 
        } catch (InterruptedException e) {
            // Ignore and allow thread to exit
        }
    }
}

最后是应用程序代码:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class AsyncExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        UI ui = new UI();
        DataProducer producer = new DataProducer(ui);
        producer.start();
        Scene scene = new Scene(ui.getView(), 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

运行它,我看到UI生成后大约0.1毫秒处理的数据符合您的要求。 (前一个或两个将花费更长时间,因为它们是在start方法完成之前和UI物理显示之前生成的,因此他们对Platform.runLater(...)的调用将需要等待该工作完成。)

此代码的问题当然是DataProducer与UI和JavaFX紧密耦合(直接使用Platform类)。您可以通过为一般消费者提供处理数据来删除此耦合:

import java.util.Random;
import java.util.function.Consumer;

public class DataProducer extends Thread {

    private final Consumer<MyDataClass> dataConsumer ;

    public DataProducer(Consumer<MyDataClass> dataConsumer) {
        this.dataConsumer = dataConsumer ;
        setDaemon(true);
    }

    @Override
    public void run()  {
        Random rng = new Random();
        try {
            while (true) {
                MyDataClass data = new MyDataClass(rng.nextInt(100));
                dataConsumer.accept(data);
                Thread.sleep(rng.nextInt(1000) + 250);
            } 
        } catch (InterruptedException e) {
            // Ignore and allow thread to exit
        }
    }
}

然后

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class AsyncExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        UI ui = new UI();
        DataProducer producer = new DataProducer(d -> Platform.runLater(() -> ui.registerData(d)));
        producer.start();
        Scene scene = new Scene(ui.getView(), 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

请注意,此处设置Consumer与提供事件处理程序非常相似:消费者被通知&#34;或者&#34;触发&#34;每当生成数据元素时。如果您希望通知多个不同的视图,则可以轻松地将其扩展为List<Consumer<MyDataClass>>,并将消费者添加/删除到该列表。数据类型MyDataClass扮演事件对象的角色:它包含有关所发生事件的确切信息。 Consumer是一个通用的功能接口,因此它可以由您选择的任何类实现,也可以由lambda表达式实现(如本例中所示)。

作为此版本的变体,您可以通过将Platform.runLater(...)抽象为Consumer(这只是运行的东西)将Platform.runLater(...)java.util.concurrent.Executor的执行分离开来Runnable S):

import java.util.Random;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

public class DataProducer extends Thread {

    private final Consumer<MyDataClass> dataConsumer ;
    private final Executor updateExecutor ;

    public DataProducer(Consumer<MyDataClass> dataConsumer, Executor updateExecutor) {
        this.dataConsumer = dataConsumer ;
        this.updateExecutor = updateExecutor ;
        setDaemon(true);
    }

    @Override
    public void run()  {
        Random rng = new Random();
        try {
            while (true) {
                MyDataClass data = new MyDataClass(rng.nextInt(100));
                updateExecutor.execute(() -> dataConsumer.accept(data));
                Thread.sleep(rng.nextInt(1000) + 250);
            } 
        } catch (InterruptedException e) {
            // Ignore and allow thread to exit
        }
    }
}

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class AsyncExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        UI ui = new UI();
        DataProducer producer = new DataProducer(ui::registerData, Platform::runLater);
        producer.start();
        Scene scene = new Scene(ui.getView(), 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

解耦类的另一种方法是使用BlockingQueue来传输数据。这具有可以限制队列大小的功能,因此如果有太多数据待处理,数据生成线程将阻塞。此外,您可以批量处理&#34; UI类中的许多数据更新,如果您足够快地生成它们以使更多的更新涌入FX应用程序线程,这将非常有用(我不会在此处显示该代码;您需要在a中使用数据) AnimationTimer并进一步放松你的概念&#34;立即&#34;)。这个版本看起来像:

import java.util.Random;
import java.util.concurrent.BlockingQueue;

public class DataProducer extends Thread {

    private final BlockingQueue<MyDataClass> queue ;

    public DataProducer(BlockingQueue<MyDataClass> queue) {
        this.queue = queue ;
        setDaemon(true);
    }

    @Override
    public void run()  {
        Random rng = new Random();
        try {
            while (true) {
                MyDataClass data = new MyDataClass(rng.nextInt(100));
                queue.put(data);
                Thread.sleep(rng.nextInt(1000) + 250);
            } 
        } catch (InterruptedException e) {
            // Ignore and allow thread to exit
        }
    }
}

UI还有一些工作要做:它需要一个线程来重复从队列中获取元素。请注意,queue.take()会阻塞,直到有可用的元素:

import java.util.concurrent.BlockingQueue;

import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;

public class UI {

    private final TextArea textArea ;
    private final Parent view ;
    private long total ;
    private long count ;
    private final DoubleProperty average = new SimpleDoubleProperty(0);


    public UI(BlockingQueue<MyDataClass> queue) {
        textArea = new TextArea();

        Label aveLabel = new Label();
        aveLabel.textProperty().bind(average.asString("Average: %.3f"));

        view = new BorderPane(textArea, null, null, aveLabel, null);

        // thread to take items from the queue and process them:

        Thread queueConsumer = new Thread(() -> {
            while (true) {
                try {
                    MyDataClass data = queue.take();
                    Platform.runLater(() -> registerData(data));
                } catch (InterruptedException exc) {
                    // ignore and let thread exit
                }
            }
        });
        queueConsumer.setDaemon(true);
        queueConsumer.start();
    }

    public void registerData(MyDataClass data) {
        textArea.appendText(String.format("Data: %d (received %.3f milliseconds after generation)%n", 
                data.getValue(), data.age()/1_000_000.0)); 
        count++;
        total+=data.getValue();
        average.set(1.0*total / count);
    }

    public Parent getView() {
        return view ;
    }
}

然后你就做了

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class AsyncExample extends Application {

    private final int MAX_QUEUE_SIZE = 10 ;

    @Override
    public void start(Stage primaryStage) {

        BlockingQueue<MyDataClass> queue = new ArrayBlockingQueue<>(MAX_QUEUE_SIZE);
        UI ui = new UI(queue);
        DataProducer producer = new DataProducer(queue);
        producer.start();
        Scene scene = new Scene(ui.getView(), 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

同样,所有这些版本只需使用Platform.runLater(...)来安排更新(只有不同​​的解耦类机制)。实际上,这至少在概念上是将runnable放入一个无界的队列中; FX Application Thread从此队列中获取元素并运行它们(在该线程上)。因此,一旦FX应用程序线程有机会执行runnable,这实际上就是你可以实现的。

听起来你需要生成要阻塞的数据的线程,直到数据处理完毕,但是如果你需要的话也可以实现(例如,只需将队列大小设置为1) )。

答案 1 :(得分:1)

在此处阅读问题和答案:(refresh label not working correctly javafx

在此处阅读问题和答案:(Queue print jobs in a separate single Thread for JavaFX

以上是使用BlockingQueue的问题和答案。

这里的教程和理论:http://tutorials.jenkov.com/java-util-concurrent/blockingqueue.html