物理线程与AnimationTimer同步

时间:2015-08-23 08:51:59

标签: multithreading javafx

问题

物理线程正在写入通用数据结构。 AnimationTimer呈现公共数据结构的数据。物理线程以30fps运行,AnimationTimer以60fps运行。

显然,你需要在这两者之间进行某种同步。当物理线程写入时,AnimationTimer不应该使用公共数据结构的数据。反之亦然。

问题

将物理线程与AnimationTimer同步的首选方法是什么?

显而易见的方法是使用多个数据结构。但问题仍然存在:如何在不阻塞fx线程的情况下正确同步它们中的任何一个数据结构?

代码

这里有一些代码,以防有人想要玩弄。它是一个虚拟实现,其中物理线程和渲染器访问相同的数据结构。每个物理帧添加一个新数据点。每个渲染器框架的所有数据点都绘制在画布上。每次渲染帧发生冲突时都会写入日志行。

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class Main extends Application {

    double sceneWidth = 640;
    double sceneHeight = 480;

    Canvas canvas;

    /**
     * The data structure is filled in the physics thread, used in the render thread 
     * Values: 0 = don't paint pixel, 1 = paint pixel
     */
    double[] commonDataStructure = new double[(int) (sceneWidth * sceneHeight)];

    /**
     * True when the physics thread is performing its calculations and writing data to the common data structure
     */
    boolean isPhysicsThreadWritingData = false;

    @Override
    public void start(Stage primaryStage) {

        BorderPane root = new BorderPane();

        // create canvas to paint on
        canvas = new Canvas( sceneWidth, sceneHeight);
        root.setCenter(canvas);

        Scene scene = new Scene(root, sceneWidth, sceneHeight);

        primaryStage.setScene(scene);
        primaryStage.show();

        startPhysics();
        startRender();

    }

    /**
     * Physics thread running at 30fps
     */
    private void startPhysics() {

        Thread physicsThread = new Thread(new Runnable() {

            double physicsFps = 1000f / 30f;
            int physicsFrameCount = 0; // counter used for adding new data point per physics frame

            @Override
            public void run() {

                long prevTime = System.currentTimeMillis();
                long currTime = System.currentTimeMillis();
                while (true) {

                    currTime = System.currentTimeMillis();

                    // run only at required physics fps
                    if ((currTime - prevTime) >= physicsFps) {

                        physicsFrameCount++;

                        if( physicsFrameCount > commonDataStructure.length) {
                            physicsFrameCount = 0;
                        }

                        // perform physics calculations
                        calculatePhysicsData( physicsFrameCount);

                        prevTime = currTime;
                    }
                }
            }
        });

        physicsThread.setDaemon(true);
        physicsThread.start();

    }

    /**
     * Render loop
     */
    private void startRender() {

        AnimationTimer renderLoop = new AnimationTimer() {

            @Override
            public void handle(long now) {

                // render on canvas using the physics data
                renderUsingPhysicsData();

            }
        };

        renderLoop.start();

    }

    /**
     * Dummy physics implementation which adds a new data point at every frame
     * @param physicsFrameCount
     */
    private void calculatePhysicsData( int physicsFrameCount) {

        isPhysicsThreadWritingData = true;

        for( int i=0; i < commonDataStructure.length; i++) {

            if( i < physicsFrameCount) {
                commonDataStructure[ i] = 1;
            } else {
                commonDataStructure[ i] = 0;
            }

        }

        isPhysicsThreadWritingData = false;

    }

    /**
     * Dummy render implementation which reads the common data and paints it (as single pixel) on a canvas.
     */
    private void renderUsingPhysicsData() {

        // log only once per frame to avoid flood logging
        boolean isConflictLogged = false;

        GraphicsContext gc = canvas.getGraphicsContext2D();

        // clear screen
        gc.setFill(Color.BLACK);
        gc.fillRect(0, 0, sceneWidth, sceneHeight);

        // paint
        gc.setFill(Color.YELLOW);
        for( int i=0; i < commonDataStructure.length; i++) {

            // check if we are rendering data that are being modified
            if( isPhysicsThreadWritingData && !isConflictLogged) {
                System.err.println( "Physics thread is writing while data are still being rendered");
                isConflictLogged = true;
            }

            double x = i % sceneWidth;
            double y = i / sceneWidth;

            if( commonDataStructure[i] != 0) {
                gc.fillRect(x, y, 1, 1);
            }
        }

    }

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

}

非常感谢您的帮助!

1 个答案:

答案 0 :(得分:2)

低级同步很难做到正确。例如,您需要使isPhysicsThreadWritingData volatile(或同步对它的访问)从不同的线程读取和写入它,以确保两个线程都能看到正确的“实时”值。对此(IMO)的最佳快速概述是Joshua Bloch的 Effective Java 中的并发部分。

最好尽可能使用更高级别的API。如果可能的话,我建议让您的数据结构不可变,为它创建atomic wrapper。例如:

public class PhysicalState {

    private final double[] data ;

    public PhysicalState(double[] data) {
        this.data = data ;
    }

    public double[] getData() {
        double[] dataCopy = new double[data.length];
        System.arraycopy(data, 0, dataCopy, 0, data.length);
        return dataCopy ;
    }

    public int getNumberOfDataPoints() {
        return data.length;
    }

}

然后您可以按如下方式修改代码:

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.concurrent.atomic.AtomicReference ;

public class Main extends Application {

    int sceneWidth = 640;
    int sceneHeight = 480;

    Canvas canvas;

    /**
     * The data structure is filled in the physics thread, used in the render thread 
     * Values: 0 = don't paint pixel, 1 = paint pixel
     */

    AtomicReference<PhysicalState> state = new AtomicReference<>(new PhyiscalState(new double[sceneWidth * sceneHeight]));


    @Override
    public void start(Stage primaryStage) {

        BorderPane root = new BorderPane();

        // create canvas to paint on
        canvas = new Canvas( sceneWidth, sceneHeight);
        root.setCenter(canvas);

        Scene scene = new Scene(root, sceneWidth, sceneHeight);

        primaryStage.setScene(scene);
        primaryStage.show();

        startPhysics();
        startRender();

    }

    /**
     * Physics thread running at 30fps
     */
    private void startPhysics() {

        Thread physicsThread = new Thread(new Runnable() {

            double physicsFps = 1000f / 30f;
            int physicsFrameCount = 0; // counter used for adding new data point per physics frame

            @Override
            public void run() {

                long prevTime = System.currentTimeMillis();
                long currTime = System.currentTimeMillis();
                while (true) {

                    currTime = System.currentTimeMillis();

                    // run only at required physics fps
                    if ((currTime - prevTime) >= physicsFps) {

                        physicsFrameCount++;

                        if( physicsFrameCount > state.get().getNumberOfDataPoints()) {
                            physicsFrameCount = 0;
                        }

                        // perform physics calculations
                        calculatePhysicsData( physicsFrameCount);

                        prevTime = currTime;
                    }
                }
            }
        });

        physicsThread.setDaemon(true);
        physicsThread.start();

    }

    /**
     * Render loop
     */
    private void startRender() {

        AnimationTimer renderLoop = new AnimationTimer() {

            @Override
            public void handle(long now) {

                // render on canvas using the physics data
                renderUsingPhysicsData();

            }
        };

        renderLoop.start();

    }

    /**
     * Dummy physics implementation which adds a new data point at every frame
     * @param physicsFrameCount
     */
    private void calculatePhysicsData( int physicsFrameCount) {

        double[] newData = new double[state.get().getNumberOfDataPoints()];
        for( int i=0; i < newData.length; i++) {

            if( i < physicsFrameCount) {
                newData[ i] = 1;
            } else {
                newData[ i] = 0;
            }

        }

        state.set(new PhysicalState(newData));

    }

    /**
     * Dummy render implementation which reads the common data and paints it (as single pixel) on a canvas.
     */
    private void renderUsingPhysicsData() {

        GraphicsContext gc = canvas.getGraphicsContext2D();

        // clear screen
        gc.setFill(Color.BLACK);
        gc.fillRect(0, 0, sceneWidth, sceneHeight);

        // paint
        gc.setFill(Color.YELLOW);

        double[] data = state.get().getData();

        for( int i=0; i < data.length; i++) {

            double x = i % sceneWidth;
            double y = i / sceneWidth;

            if( data[i] != 0) {
                gc.fillRect(x, y, 1, 1);
            }
        }

    }

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

}

同样,您应该考虑使用SceheduledExecutorService来管理每秒运行物理引擎30次。

这可能是最简单的方法,但不一定是性能最高的,因为正在进行一些阵列复制,可以使用其他技术来避免。

其他方法可能涉及“管道”,其中线程将数据放入共享BlockingQueue并从中检索数据。例如,你可以让你的“物理线程”计算数据数组并将其推送到大小为1的BlockingQueue,只要有可用的东西就从BlockingQueue获取“画布生成线程”,创建Canvas,并设置AtomicReference<Canvas>。然后AnimationTimer只显示画布的当前值。 SO用户@jewelsea有几个很好的例子来说明这些方法herehere