0

概括

我正在做一个需要低 delta_t 的实时物理模拟。我已将此模拟连接到 python-arcade 游戏窗口以实时显示信息。

我为物理线程制作了一个单独的线程,因为物理线程中有一些昂贵的矩阵乘法。然后,当更新完成时,我设置游戏窗口类的结果状态,游戏窗口在绘制新框架时可以显示这些状态。

因此,我的想法是游戏窗口类只需要担心在屏幕上的绘制,而物理线程负责所有的计算。

但是,游戏窗口和线程之间的通信存在瓶颈,我不知道是否有深入的了解。

我想做的最小表示:

import threading
import time
import math
import arcade


class DisplayWindow(arcade.Window):
    def __init__(self):
        super().__init__(width=400, height=400)

        self.state = 0
        self.FPS = 0

    def set_state(self, state):
        self.state = state

    def on_update(self, delta_time: float):
        self.FPS = 1. / delta_time

    def on_draw(self):
        arcade.start_render()
        arcade.draw_text(f'FPS: {self.FPS:0.2f}', 20, 20, arcade.color.WHITE)
        arcade.draw_rectangle_filled(center_x=self.state * self.width,
                                     center_y=self.height/2,
                                     color=arcade.color.WHITE,
                                     tilt_angle=0,
                                     width=10,
                                     height=10)

# Thread to simulate physics.
def simulation(display):
    t_0 = time.time()
    while True:

        # Expensive calculation that needs high frequency:
        t = time.time() - t_0
        x = math.sin(t) / 2 + 0.5       # sinusoid for demonstration

        # Send it to the display window
        display.set_state(state=x)

        # time.sleep(0.1)               # runs smoother with this

def main():
    display_window = DisplayWindow()
    physics_thread = threading.Thread(target=simulation, args=(display_window,), daemon=True)
    physics_thread.start()

    arcade.run()

    return 0

if __name__ == '__main__':
    main()

预期结果:高帧率的平滑模拟。街机窗口只需以 30 或 60 fps 运行 on_draw。它只需要画一些东西。

实际结果:物理循环运行速度超快并调用 FPS 下降。

当我在物理线程中添加 time.sleep(0.1) 时,整个事情变得更加顺畅,我猜出于某种原因set_state( _ )会减慢绘制循环。

4

2 回答 2

1

Python 线程可能不是您尝试做的工作的理想工具。

尽管认为 Python 线程并发运行可能很诱人,但事实并非如此:全局解释器锁 (GIL) 只允许一个线程控制 Python 解释器。更多信息

正因为如此,该arcade.Window对象无法及早控制 Python 解释器并运行其所有更新函数,因为 GIL 始终“专注”simulationphysics_thread.

GIL 只会在physics_thread运行了一定数量的指令或使用which 在线程上执行physics_thread设置为睡眠后才释放对其他线程的关注并在其他线程上执行其他操作。这正是您凭经验发现的恢复程序预期行为的方法。time.sleep()

这是一个称为线程饥饿的典型问题的示例,可以通过使用多处理库来解决。这会带来更多的复杂性,但会将您的 CPU 密集型计算和基于事件的轻量级接口分开在不同的进程中,从而解决您的问题。

于 2019-09-23T12:22:32.430 回答
0

感谢@pjmv 的回答,我研究了使用多处理而不是线程

multiprocessing.Pipe对象确保了双工通信并使整个事情变得更加顺畅。我现在还可以确保模拟的实时运行。

两边的每个更新循环,只需使用send()andrecv()命令。尚未测试边缘情况,但似乎工作顺利。

我将修改添加到上面发布的示例中:

import time
import arcade
from multiprocessing import Process, Pipe
from math import sin, pi


class DisplayWindow(arcade.Window):
    def __init__(self, connection: Pipe):
        super().__init__(500, 500)

        self.connection: Pipe = connection    # multiprocessing.Pipe

        self.position: float = 0               # GUI Display state
        self.user_input: float = 1.0           # Input to simulation
        self.FPS: float = 0                    # Frames per second estimation

    def on_update(self, delta_time: float):
        self.FPS = 1. / delta_time

        # Communicate with simulation:
        self.connection.send(self.user_input)
        self.position = self.connection.recv()

    def on_draw(self):
        arcade.start_render()
        arcade.draw_text(f'FPS: {self.FPS:0.0f}', 20, 20, arcade.color.WHITE)
        arcade.draw_point(self.position, self.height/2, arcade.color.WHITE, 10)

    def on_key_release(self, symbol: int, modifiers: int):
        if symbol == arcade.key.W:
            self.user_input = 1.8
        elif symbol == arcade.key.S:
            self.user_input = 0.3


# Separate Process target to simulate physics:
def simulation(connection: Pipe):
    t_0 = time.time()
    while True:
        freq = connection.recv() * 2 * pi       # Receive GUI user input

        t = time.time() - t_0
        x = sin(freq * t) * 250 + 250

        connection.send(x)                      # Send state to GUI

def main():
    parent_con, child_con = Pipe()
    display_window = DisplayWindow(connection=parent_con)
    physics = Process(target=simulation, args=(child_con,), daemon=True)
    physics.start()
    arcade.run()
    physics.terminate()
    return 0

if __name__ == '__main__':
    main()
于 2019-09-23T19:46:18.397 回答