清除具有大量形状/多线程的组的最快方法

Posted

技术标签:

【中文标题】清除具有大量形状/多线程的组的最快方法【英文标题】:Fastest way to clear group with a lot of shapes / multithreading 【发布时间】:2022-01-12 20:47:57 【问题描述】:

在我的 JavaFX 项目中,我使用了很多形状(例如 1 000 000)来表示地理数据(例如地块轮廓、街道等)。它们存储在一个组中,有时我必须清除它们(例如,当我加载包含新地理数据的新文件时)。 问题:清除/删除它们需要很多时间。 所以我的想法是在一个单独的线程中删除形状,由于 JavaFX 单线程,这显然不起作用。

这是我正在尝试做的简化代码:

HelloApplication.java

package com.example.javafxmultithreading;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.shape.Line;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application 

    public static Group group = new Group();

    @Override
    public void start(Stage stage) throws IOException 
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load());
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();

        for (int i = 0; i < 1000000; i++) 
            group.getChildren().add(new Line(100, 200, 200, 300));
        
        HelloController.helloController = fxmlLoader.getController();
        HelloController.helloController.pane.getChildren().addAll(group);
    

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

HelloController.java

public class HelloController 

    public static HelloController helloController;
    @FXML
    public Pane pane;
    public VBox vbox;

    @FXML
    public void onClearShapes() throws InterruptedException 
        double start = System.currentTimeMillis();
        HelloApplication.group.getChildren().clear();
        System.out.println(System.currentTimeMillis() - start);

        Service<Boolean> service = new Service<>() 
            @Override
            protected Task<Boolean> createTask() 
                return new Task<>() 
                    @Override
                    protected Boolean call() 
                        // Try to clear the children of the group in this thread
                        return true;
                    
                ;
            
        ;
        service.setOnSucceeded(event -> 
            System.out.println("Success");
        );
        service.start();
    

你好-view.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox fx:id="vbox" alignment="CENTER" prefHeight="465.0" prefWidth="711.0" spacing="20.0"
      xmlns="http://javafx.com/javafx/11.0.2" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="com.example.javafxmultithreading.HelloController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>
    <Pane fx:id="pane" prefHeight="200.0" prefWidth="200.0"/>
    <Button mnemonicParsing="false" onAction="#onClearShapes" text="Clear shapes"/>
</VBox>

我用group.getChildren().clear()测量了从组中删除不同数量的孩子所花费的时间:

amount of children    |   time
100                       2ms = 0,002s
1 000                     4ms = 0,004s
10 000                    38ms = 0,038s
100 000                   1273ms = 1,2s
1 000 000                 149896ms = 149,896s = ~2,5min

如您所见,所需时间呈指数级增长。 现在想象一下,您必须在 UI 中清除子项,而用户必须等待 2.5 分钟等待应用程序冻结。 此外,在这个简化的示例中,它只是一条简单的线,在“真实”应用程序中,它是一个更复杂的几何图形 -> 需要更多时间。

因此,另一个想法是将组与它的父级窗格“解除绑定”。 因为当它解除绑定时,我可以在另一个线程中删除它。这意味着 1. 用户界面不会冻结 2. 它会更快。 这是尝试:

pane.getChildren().remove(group); // or clear()
// and then clear the group in another thread like above

问题:这种“解除绑定”也需要很多时间。不是 2.5 分钟,而是 0.5 分钟,这仍然是太多了。

另一个想法是创建多个组,因为如您所见,具有 10 000 或 100 000 个元素的组被清除得更快。 这也失败了,因为几个组突然需要更长的时间并且被删除的速度呈指数级增长。 例如,第一个需要 20 秒,第二个需要 10 秒,第三个需要 5 秒等。

长话短说

有没有机会在单独的线程中删除该组的孩子,或者比group.getChildren().clear() 更快?我尝试了所有想到的...

如果我只能在删除时显示一个加载栏,那比仅仅冻结表面并等待 2 分钟要好...

我感谢每一个想法/帮助。

编辑,见 cmets 没有 FXML 的简单示例:

import javafx.scene.Group;
import javafx.scene.shape.Line;

public class Test 

    public static void main(String[] args) 
        Group group = new Group();
        System.out.println("adding lines");
        for (int i = 0; i < 1000000; i++) 
            group.getChildren().add(new Line(100, 200, 200, 300));
        
        System.out.println("adding done");

        System.out.println("removing starts");
        double start = System.currentTimeMillis();
        group.getChildren().clear();
        System.out.println("removing done, needed time: " + (System.currentTimeMillis() - start));
    

【问题讨论】:

clear() 的简单调用似乎不太可能真的需要那么长时间。 (这可能表明存在某种错误。)请注意,您不能从后台线程修改活动节点(作为场景图一部分的节点),所以我真的看不出线程在这里有什么帮助。您能否发布一个完整、简单的示例,我们可以通过简单的尝试来复制和运行(并且没有所有静态字段等)?我不明白为什么标准方法行不通。 “很多形状(例如 1 000 000)来表示地理数据” -> 只是我的意见,但我认为这不是一个好的设计。形状是为屏幕渲染而设计的节点。使用节点来表示此类数据而不是专门针对该类型数据的数据结构存在开销。我不了解 GIS 系统,但我确信它们具有使用 Java 绑定的数据表示形式,这对于此类数据比使用节点更有效。最终,您需要映射到可见节点,但不应为 100 万。 我只是不明白您为什么将Group 定义为公共和静态,并在Application 类中,然后从那里操作控制器中的节点。这不是“正常”的 JavaFX(甚至 Java)编程风格。您从标准实践中获得的越多,您就越有可能陷入一些奇怪的极端情况,因为某些奇怪的原因会影响性能。 (虽然这似乎不太可能是这里的原因。)无论如何,我会在回到电脑前进行实验 @James_D 从分析来看,时间似乎都花在了parent 属性的invalidated() 方法上(当一个孩子被移除时,父级设置为null)。特别是,从 parent 的 treeVisibledisabled 属性中删除侦听器似乎很昂贵。 有 1,000,000 个子属性,这意味着这些属性每个都至少注册了 1,000,000 个侦听器。这些侦听器存储在一个数组中,并在清除所有 1,000,000 个子项时一个接一个删除。这意味着这项工作至少每个属性需要进行 1,000,000 次线性搜索(当然,搜索时间会随着删除的子项的处理而减少,但仍然如此)。 【参考方案1】:

执行时间长的原因是Parent 的每个子对象都使用ParentdisabledtreeVisible 属性注册了一个侦听器。 JavaFX 当前的实现方式,这些监听器存储在一个数组中(即一个列表结构)。 添加监听器的成本相对较低,因为新监听器只是简单地插入到数组的末尾,偶尔会调整数组的大小。但是,当您从其Parent删除一个子项并删除侦听器时,需要线性搜索数组,以便找到并删除正确的侦听器。这会发生在每个被移除的孩子身上。

因此,当您清除 Group 的子列表时,您将触发对这两个属性的 1,000,000 次线性搜索,总共会产生 2,000,000 次线性搜索。更糟糕的是,删除的子节点可能是从添加的第一个子节点到最后添加的子节点的处理过程,这意味着被删除的侦听器可能始终位于数组的末尾。所以不仅是 2,000,000 次线性搜索,而且是 2,000,000 次最坏情况线性搜索。

至少有两种解决方案/变通方法:

    不要显示 1,000,000 个节点。如果可以,请尝试仅显示用户实际可以看到的数据的节点。例如,ListViewTableView 等虚拟化控件在任何给定时间仅显示大约 1-20 个单元格。

    不要清除Group 的子代。相反,只需将旧的Group 替换为新的Group。如果需要,您可以在后台线程中准备新的Group

    这样做,我用了 3.5 秒的时间在我的电脑上创建了另一个有 1,000,000 个孩子的Group,然后用新的Group 替换旧的Group。但是,由于需要一次渲染所有新节点,因此仍然存在一些延迟峰值。

    如果您不需要填充新的Group,那么您甚至不需要线程。在这种情况下,我的计算机上的交换大约需要 0.27 秒。

【讨论】:

非常感谢您的详细解答。我应该如何替换Group?就像我上面的 FXML 示例一样,有一个 Pane 和一个 Group。如果我尝试用HelloApplication.group = new Group(); 替换删除语句,旧组仍然是Pane 的子组,而新组不是。 您必须删除旧组并添加新组。如果您的组是窗格的唯一子级,那么您只需执行pane.getChildren().setAll(newGroup) 好的,这工作非常快,谢谢!不幸的是,这不适用于我的“真实”代码,setAll 仍然需要 73 秒。但现在取决于我的代码,所以我会尝试一些东西并找出问题所在。还有一个问题:如何查看 Java 库中的代码?那么,例如,当调用 setAll 时会发生什么? (我正在使用 Intellij) @Enrico 您也可以下载 JavaFX 的源代码并将它们附加到您的 IDE,或者更好地指示您的 Maven/Gradle 构建自动下载和附加源代码。 关于您的原始问题,我仍然不知道您为什么认为必须将这么多单个节点添加到场景图中。我也从事地理业务,只是没有看到有效的用例。您是否考虑过路径或其他表示图形元素的方式?你能举一个你真正想画的例子吗?

以上是关于清除具有大量形状/多线程的组的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

java 多线程 25 :线程和线程组的异常处理

具有最快(并发)添加操作的集合

搜索硬盘中所有文件的最快方法是啥?

最快实现单线程提供数据,多线程消费数据

05.java多线程问题

Java多线程