Как заставить высокочастотный поток общаться с классом, который обновляется с низкой частотой? - PullRequest
0 голосов
/ 23 сентября 2019

Резюме

Я делаю физическое моделирование в реальном времени, для которого требуется низкий delta_t.Я подключил эту симуляцию к окну аркадной игры с питоном для отображения информации в реальном времени.

Я создал отдельный поток для физики, потому что в потоке физики есть несколько дорогостоящих умножений матриц.Затем, когда обновление выполнено, я устанавливаю результирующие состояния класса игрового окна, которое игровое окно может отображать всякий раз, когда оно рисует новый кадр.

Следовательно, мой мыслительный процесс заключается в том, что класс игрового окна должен заботиться только о рисовании на экране, в то время как физический поток заботится обо всех вычислениях.

Однако естьузкое место в общении между окном игры и потоком, и я не знаю, что у меня есть понимание.

Минимальное представление того, что я хочу сделать:

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()

Ожидаемый результат: Плавное моделирование с высокой частотой кадров.Аркадное окно должно запускать on_draw только со скоростью 30 или 60 кадров в секунду.Нужно только нарисовать несколько вещей.

Фактический результат: Физический цикл работает очень быстро и вызывает падение FPS.

Когда ядобавьте time.sleep (0.1) к физическому потоку, все становится намного более плавным, я думаю, по какой-то причине set_state( _ ) замедляет цикл рисования.

Ответы [ 2 ]

1 голос
/ 23 сентября 2019

Потоки Python могут быть не идеальным инструментом для работы, которую вы пытаетесь сделать.

Хотя может показаться заманчивым думать, что потоки Python работают одновременно, это не так: Global Interpreter Lock (GIL)только один поток может управлять интерпретатором Python. Подробнее

Из-за этого у объекта arcade.Window нет раннего шанса управлять интерпретатором Python и запускать все его функции обновления, потому что GIL остается "сфокусированным" на бесконечностиЦикл в функции simulation вашего physics_thread.

GIL освободит фокус на physics_thread и будет искать что-то еще для других потоков после того, как будет выполнено определенное количество инструкций или physics_thread переведен в спящий режим с использованием time.sleep() который выполняет на потоках .Это именно то, что вы эмпирически нашли для восстановления ожидаемого поведения программы.

Это пример типичной проблемы, называемой истощением потоков, которая может быть решена с помощью многопроцессорной библиотеки .Это немного сложнее, но разделяет ваши ресурсоемкие вычисления и облегченный интерфейс на основе событий на отдельные процессы, решая тем самым вашу проблему.

0 голосов
/ 23 сентября 2019

Благодаря ответу @ pjmv я решил использовать многопроцессорную обработку вместо многопоточности .

Объект multiprocessing.Pipe обеспечиваетдуплексная связь и сделал все это более гладким.Теперь я могу также обеспечить выполнение симуляции в режиме реального времени.

В каждом цикле обновления с обеих сторон просто используйте команды send() и recv().Пока не проверял крайние случаи, но, кажется, работает без сбоев.

Я добавил модификацию к приведенному выше примеру:

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()
...