问题
物理线程正在写入通用数据结构。 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);
}
}
非常感谢您的帮助!
答案 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有几个很好的例子来说明这些方法here和here。