当前位置: 首页 > 面试题库 >

在GUI线程中触发异步事件

平学
2023-03-14
问题内容

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

编辑:
我说“立即”一词,正如一些评论家指出的那样,这是不可能的。我的意思是,如果gui线程处于空闲状态,则应该在毫秒到十亿分之一秒的范围内相当快地发生(这应该是正确的,如果我做对的话)。

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

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

“观察者模式”的其他解决方案通常包括一个观察者,该观察者坐在while循环中,检查一些同步队列中是否有新数据。这不适用于我的应用程序,因为Javafx要求仅从gui线程修改gui元素,并且必须很大程度上不占用gui线程,以便它有时间重绘内容并响应用户事件。循环将导致应用程序挂起。

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

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


问题答案:

听起来,您实际上真正需要的就是通过调用FX Application
Thread上的UI更新Platform.runLater(...)。这将安排一个更新,该更新将在FX Application
Thread有时间后立即执行,只要您没有过多的请求就可以很快地执行更新。下次出现渲染脉冲时,更新将对用户可见(因此从用户的角度来看,此更新将尽快发生)。

这是一个最愚蠢的示例:产生数据的异步类直接在UI上安排更新。

首先是一个简单的类来保存一些数据。我添加了一些功能来检查数据的“年龄”,即自构造函数被调用以来已经过了多长时间:

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

运行此命令,我会在生成数据后约0.1毫秒内看到UI正在处理的数据,这符合您的要求。(第一个或第二个将花费更长的时间,因为它们是在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此处设置a
与提供事件处理程序非常相似:每当生成数据元素时,都会“通知”或“触发”使用者。List<Consumer<MyDataClass>>如果您想通知多个不同的视图,可以轻松地将其扩展为一个,然后将使用者添加/删除到该列表中。数据类型MyDataClass扮演事件对象的角色:它包含有关发生的确切信息。Consumer是通用的功能接口,因此可以通过您选择的任何类或lambda表达式(如本例中所做的那样)来实现。

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

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

解耦类的另一种方法是使用a
BlockingQueue传输数据。它具有可以限制队列大小的功能,因此,如果有太多待处理数据,则数据生成线程将阻塞。此外,您可以在UI类中“批量处理”许多数据更新,这在您以足够快的速度生成它们以使FX
Application
Thread充满太多更新时非常有用(我在这里没有显示该代码;您需要使用a中的数据,AnimationTimer并进一步放松“立即”的概念。这个版本看起来像:

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(...)计划更新来工作(存在各种将类解耦的机制)。至少从概念上讲,这实际上是将可运行对象放入一个无界队列中。FX
Application Thread从此队列中获取元素并运行它们(在该线程上)。因此,只要FX Application
Thread有机会,就立即执行runnable,这实际上是您可以实现的。

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



 类似资料:
  • 我有一个UserControl上的事件,我正在听: 具有以下签名: 在以下情况下,如何调用此异步方法: 不阻塞GUI线程 在调用我的异步方法后能够调用 能够在正确的(GUI)线程中调用。 我所尝试的: -- -- --

  • 我试图实现一个自定义滑块,上面有一个矩形条,线条表示特定时间点的事件,如下图所示。它基本上是滑块上方的时间线,该事件被描述为垂直线。 该图显示了滑块的开始(t=0)和结束(t=x)之间的14条垂直线。每一行对应一个带有一些上下文信息的任意事件。 滑块将实时播放,即拇指将以实时速度沿着其轨道移动。当滑块超过事件的指定时间时,它将在文本区域中打印事件的上下文信息。使用该关系图,当前正在显示14秒的事件

  • 问题内容: 我在中有一个(),如果按下该按钮,我将在for循环中对其列表中的列表执行任务。这样做时,我需要更新。 问题是,当我按下JButton时,该任务是在事件调度线程(EDT)中执行的。因此,我无法更新在主线程或UI线程中触发的事件。 现在,源代码对我不可用,因为我完全更改了源代码,并尝试使用Eclipse SWT 触发Swing时,它变得混乱。 现在我得到了错误,因为Display对象在单独

  • 我有一个Primeface应用程序,其中我启动了一个包含表单的对话框。对话框有一个保存/取消按钮对。在这个对话框中,我无法调用on完成、onstart等方法。我在其他回复中看到原因是没有执行AJAX。但是,我不知道这种情况下的原因,因为命令按钮的类型是提交,并且应该有ajax="true"(默认情况下)。有人能在里面放一点光吗? 我的xhtml: 这是我的后盾: 谢谢!

  • 线程中使用 java.lang.Runnable 如果用户在代码中通过 java.lang.Runnable 新启动了线程或者采用了线程池去异步地处理一些业务,那么需要将 SOFATracer 日志上下文从父线程传递到子线程中去,SOFATracer 提供的 com.alipay.common.tracer.core.async.SofaTracerRunnable 默认完成了此操作,大家可以按照

  • 问题内容: 我的整个项目都使用(Bluebird)Promises,但是有一个使用EventEmitter的特定库。 我想要实现以下目标: 我在Promises链中读了EventEmitter的答案。这给了我一种执行’connect’事件的回调的方法。这是我到目前为止所到之处 现在如何进一步链接“ eventB”? 问题答案: 我假设您想为每个事件做不同的事情。即使由的动作触发,您也可以将其视为另