我应该使用什么方法来实现 JavaFX Canvas 多线程?

Joh*_*hew 5 java multithreading javafx-2

我正在编写一个 JavaFX 应用程序,该应用程序接收套接字上的数据点并实时可视化它们。问题是JavaFX渲染速度太慢。我有一个运行速度足够快的 Swing 实现,但我需要使用 JavaFX。

我正在工作的限制是:

  1. 可视化控件只能由 JavaFX 应用程序线程更新(我相信这是所有 JavaFX 和 Swing 应用程序所必需的)。
  2. 从人眼的角度来看,可视化应该平滑更新。每秒大约 10 次更新就足够了。每秒一次是不够的。
  3. 传入数据速率足够高(每秒大约 50 个事件,这在其他上下文中并没有那么高),并且每个事件处理的成本足够高,以至于必须在 JavaFX 应用程序线程之外的线程中接收和处理传入数据,以便GUI 不会阻塞(我相信这是许多 GUI 应用程序的普遍要求)。

到目前为止,我的方法是使用 Canvas JavaFX 节点作为可视化控件,并让接收线程安排对 Canvas 的更新以便稍后在 JavaFX 应用程序线程中运行,如下所示。

    public void onEvent(Event event) {
        ....do processing... 
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                graphics.setFill(...);
                graphics.fillRect(...);
                }});
    }
Run Code Online (Sandbox Code Playgroud)

我想到了几种可能加快速度的方法:

  1. 使用 WritableImage 而不是 Canvas 进行可视化。缺点是WritableImage/PixelWriter似乎没有很多绘图方法,例如它甚至没有fillRect。我想我必须实现我自己的版本,而且我的版本可能会更慢。
  2. 拥有一个由处理传入数据的线程拥有的 Canvas 对象。从该画布复制到作为 JavaFX 应用程序线程中场景图中的节点的画布。复制可能会使用这些代码来完成sceneCanvas.getGraphicsContext2D().drawImage(processingCanvas.snapshot(SnapshotParameters(), null) 0, 0);。这样做的缺点是我认为它不是线程安全的,并且快照调用似乎相对昂贵。
  3. 在处理传入数据的线程中渲染到 AWT BufferedImage,然后使用 SwingFXUtils.toFXImage() 从 BufferedImage 复制到 Canvas。这样做的缺点是线程语义似乎不清楚,并且使用 AWT 似乎有点愚蠢。

您能建议一些潜在的方法吗?

谢谢你!

isn*_*bad 5

我认为主要问题是您的代码将太多绘图任务推送到 FX 应用程序线程的队列中。通常,每秒 60 次绘图操作就足够了,这等于显示器的刷新率。如果您收到的“传入数据”事件多于此,您将比必要的更频繁地绘制,从而浪费 CPU。因此,您必须将数据处理与绘画分离。

一种解决方案是使用AnimationTimer. 它的handle方法将在每个动画帧中被调用,因此通常每秒 60 次。动画计时器在处理新数据时处理重绘。

// generic task that redraws the canvas when new data arrives
// (but not more often than 60 times per second).
public abstract class CanvasRedrawTask<T> extends AnimationTimer {
    private final AtomicReference<T> data = new AtomicReference<T>(null);
    private final Canvas canvas;

    public CanvasRedrawTask(Canvas canvas) {
        this.canvas = canvas;
    }

    public void requestRedraw(T dataToDraw) {
        data.set(dataToDraw);
        start(); // in case, not already started
    }

    public void handle(long now) {
        // check if new data is available
        T dataToDraw = data.getAndSet(null);
        if (dataToDraw != null) {
            redraw(canvas.getGraphicsContext2D(), dataToDraw);
        }
    }

    protected abstract void redraw(GraphicsContext context, T data);
}

// somewhere else in your concrete canvas implementation
private final RedrawTask<MyData> task = new RedrawTask<MyData>(this) {
    void redraw(GraphicsContext context, MyData data) {
        // TODO: redraw canvas using context and data
    }
}

// may be called by a different thread
public void onDataReceived(...) {
    // process data / prepare for redraw task
    // ...

    // handover data to redraw task
    task.requestRedraw(dataToDraw);
}
Run Code Online (Sandbox Code Playgroud)