在gui线程中触发异步事件 [英] Triggering asynchronous event in gui thread

查看:200
本文介绍了在gui线程中触发异步事件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

TL; DR我正在寻找一种方法让一个线程在另一个

编辑: / strong>我说立即一词,就像一些评论者指出的那样,是不可能的。我的意思是,如果gui线程空闲(如果我做我的工作,应该是),它应该在相当快的时间内发生在低毫米到纳秒的范围内。

I say the word "immediate", which is, as some commenters have pointed out, impossible. What I mean is that it should happen reasonably quickly, in the low milli to nanosecond range if the gui thread is idle (which , if I do my job right, it should be).

案例示例:
我有一个具有Parent类的项目。该Parent类创建一个子线程Gui,它包含一个javafx应用程序并实现Runnable。父母和贵族都有相同的BlockingQueue参考。

The case example: I have a project which has a Parent class. That Parent class creates a child thread 'Gui', which houses a javafx application and implements Runnable. Both Parent and Gui have a reference to the same BlockingQueue.

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

What I want to happen: I want to be able to send objects from the parent class to the Gui thread, and have the Gui receive some sort of event which immediately calls a handling function of some sort, so I then know to get one or more objects from the queue and add them to the gui.

观察者模式的其他解决方案通常涉及位于while循环中的观察者,检查新数据的一些同步队列。这对于我的应用程序来说是不行的,因为Javafx要求仅从gui线程修改gui元素,并且gui线程必须大部分被留空,以便有时间重绘事物和响应用户事件。一个循环将导致应用程序挂起。

Other solutions for the "observer pattern" typically involve an observer which sits in a while loop, checking some synchronized queue for new data. This won't work for my application because Javafx requires that gui elements be modified only from the gui thread, and that the gui thread must largely be left unoccupied, so that it has time to redraw things and respond to user events. A loop would cause the application to hang.

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

One idea that I've found which seems to have potential is to interrupt the Gui thread from the parent thread, and have that trigger some sort of event, but I couldn't find any way to make that happen.

任何想法?这种情况的最佳实践是什么?

Any ideas? What are best practices for this sort of situation?

推荐答案

真的很像你在这里需要的是调用更新到FX应用程序主题上的UI通过 Platform.runLater(...)。这将安排一个更新,一旦FX应用程序线程有时间,它将执行,这将是相当快,只要你不用太多的请求淹没它。下一次渲染脉冲发生时,用户将看到更新(因此从用户的角度来看,尽可能快的发生)。

Really all it sounds like you need here is to invoke the update to the UI on the FX Application Thread via Platform.runLater(...). This will schedule an update which will be executed as soon as the FX Application Thread has time, which will be pretty quick as long as you are not flooding it with too many requests. The update will be visible to the user the next time a rendering pulse occurs (so from the user's perspective this happens as soon as is possible).

这是一个例子这是最为重要的:产生数据的异步类直接在UI上安排更新。

Here is an example in it's barest sense: the asynchronous class producing the data directly schedules the updates on the UI.

首先是一个简单的类来保存一些数据。我添加了一些功能来检查数据的年龄,即从调用构造函数开始的时间长度:

First a simple class to hold some data. I added in some functionality for checking the "age" of the data, i.e. how long since the constructor was invoked:

MyDataClass.java

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,显示所有收到的数据,以及数据的年龄以及所有数据的平均值:

Here's a simple UI that displays all data it receives, along with the "age" of the data and an average of all data:

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,因此它可以直接安排更新:

Here's a class that (asynchronously) sleeps a lot and produces random data (kind of like my interns...). For now, it just has a reference to the UI so it can schedule updates directly:

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

最后这里是应用程序代码:

And finally here's the application code:

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毫秒,这符合您的要求。 (第一个或者两个将需要更长的时间,因为它们在开始方法完成之前和UI在物理显示之前生成,所以他们调用 Platform.runLater(...)将需要等待该工作完成。)

Running this, I see the data being processed by the UI around 0.1 milliseconds after it is generated, which meets your requirements. (The first one or two will take longer, as they are generated before the start method completes and before the UI is physically displayed, so their calls to Platform.runLater(...) will need to wait for that work to complete.)

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

The issue with this code is, of course, that the DataProducer is tightly coupled to the UI and to JavaFX (using the Platform class directly). You can remove this coupling by giving it a general consumer to process the data:

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

请注意,设置消费者这里与提供事件处理程序非常相似:每当生成数据元素时,消费者都会通知或触发。如果您想要通知多个不同的视图,并将消费者添加到该列表,您可以轻松地将其扩展为具有 List< Consumer< MyDataClass>> 数据类型 MyDataClass 起到事件对象的作用:它包含有关确切发生了什么的信息。 消费者是一个通用的功能界面,所以它可以通过你选择的任何类或者通过lambda表达式实现(就像我们在这个例子中一样)。

Note that setting a Consumer here is very similar to providing an event handler: the consumer is "notified" or "triggered" whenever a data element is generated. You could easily extend this to have a List<Consumer<MyDataClass>> if you wanted multiple different views to be notified, and add/remove consumers to that list. The data type MyDataClass plays the role of the event object: it contains the information about exactly what happened. Consumer is a general functional interface, so it can be implemented by any class you choose, or by a lambda expression (as we do in this example).

作为这个版本的一个变体,你可以从<$ c执行的 Platform.runLater(...) $ c>消费者通过抽象 Platform.runLater(...)作为 java.util.concurrent.Executor (这只是运行 Runnable s):

As a variant on this version, you can decouple the Platform.runLater(...) from the execution of the Consumer by abstracting Platform.runLater(...) as a java.util.concurrent.Executor (which is just something that runs Runnables):

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 来传输数据。这具有您可以限制队列大小的功能,因此如果有太多数据待处理,数据生成线程将阻塞。此外,您可以在UI类中批量处理许多数据更新,如果您足够快地生成足够的丰富的FX应用程序主题以及更新更新(这里不显示代码)使用 AnimationTimer 中的数据,并进一步放松您的即时概念)。这个版本看起来像:


An alternative way to decouple the classes is to use a BlockingQueue to transmit the data. This has the feature that you can limit the size of the queue, so the data producing thread will block if there is too much data pending. Additionally, you can "bulk process" many data updates in the UI class, which is useful if you are producing them quickly enough to flood the FX Application Thread with too many updates (I don't show that code here; you would need to consume the data in an a AnimationTimer and further relax your notion of "immediate"). This version looks like:

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有一些更多的工作要做:它需要一个线程来重复地从队列中获取元素。请注意,

The UI has a bit more work to do: it needs a thread to repeatedly take elements from the queue. Note that queue.take() blocks until there is an element available to 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应用程序线程从该队列中获取元素并运行它们(在该线程上)。因此,一旦FX应用程序线程有机会就可以执行runnable,这真的是你可以实现的。

Again, all these versions simply work by using Platform.runLater(...) to schedule the updates (there are just varying mechanisms of decoupling the classes). What this actually does, at least conceptually, is place the runnable into an unbounded queue; the FX Application Thread takes elements from this queue and runs them (on that thread). Thus the runnable is executed as soon as the FX Application Thread has a chance, which is really as much as you can achieve.

听起来好像您需要在数据处理之前生成数据的线程,但是如果需要,也可以实现一个例子,只是将队列大小设置为1)。

It doesn't sound like you need the thread that is producing the data to block until the data has been processed, but that can be achieved too if you need (as an example, just set the queue size to 1).

这篇关于在gui线程中触发异步事件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆