在gui线程中触发异步事件

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

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

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

我想要发生的事情:我希望能够将对象从父类发送到Gui线程,并让Gui接收某种事件,立即调用某种处理函数,所以我知道得到一个或者队列中的更多对象并将它们添加到gui中。

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

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

有任何想法吗? 针对这种情况的最佳做法是什么?

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

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

首先是一个保存一些数据的简单类。 我添加了一些function来检查数据的“年龄”,即调用构造函数后多长时间:

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 ; } } 

这是一个简单的UI,显示它收到的所有数据,以及数据的“年龄”和所有数据的平均值:

 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 dataConsumer ; public DataProducer(Consumer 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与提供事件处理程序非常相似:每当生成数据元素时,都会“通知”或“触发”使用者。 如果您希望通知多个不同的视图,并且将消费者添加/删除到该列表,则可以轻松地将其扩展为具有List> 。 数据类型MyDataClass扮演事件对象的角色:它包含有关发生的确切事件的信息。 Consumer是一个通用的function接口,因此它可以由您选择的任何类实现,也可以通过lambda表达式实现(就像我们在本例中所做的那样)。

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

 import java.util.Random; import java.util.concurrent.Executor; import java.util.function.Consumer; public class DataProducer extends Thread { private final Consumer dataConsumer ; private final Executor updateExecutor ; public DataProducer(Consumer 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传输数据。 这具有可以限制队列大小的function,因此如果有太多数据待处理,则数据生成线程将阻塞。 此外,您可以在UI类中“批量处理”许多数据更新,如果您足够快地生成它们以使更多的更新泛滥FX应用程序线程(我不在此处显示该代码,您将需要消费一个AnimationTimer的数据,进一步放松你的“立即”概念。 这个版本看起来像:

 import java.util.Random; import java.util.concurrent.BlockingQueue; public class DataProducer extends Thread { private final BlockingQueue queue ; public DataProducer(BlockingQueue 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 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 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)。

在这里阅读问题和答案:( 刷新标签无法正常工作javafx )

在这里阅读问题和答案:( 在单独的Thread for JavaFX中排队打印作业 )

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

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