Run simultaneous process inside python class - python

I'm developping a game using pygame and I want to create a loading screen while the assets are loaded. The loading screen have animations, so loading screen and assets loading should be occurring at the same time.
Consider the code below:
class Game:
def __init__(self):
self.loading_screen()
self.load_assets()
def loading_screen(self):
# do something while load_assets() is running
def load_assets(self):
# load all assets needed
I've tried Process from multiprocessing, but I dont know how to keep loading_screen() running without freezes while load_assets() are running.
Also, I've tried threads, but python doesn't run threads simultaneously, so, in some moment, the loading_screen() will freeze. (This could be wrong, but this was observed in the game)
Some help about this?
Thanks for all

Threads are suitable for this. Nothing runs exactly simultaneously on a multi user system anyway.
import threading
from time import sleep
class Game:
def __init__(self):
self.loading_screen()
def loading_screen(self):
t = threading.Thread(target=self.load_assets)
t.daemon = True
t.start()
while t.isAlive():
sleep(0.1)
print("loading screen")
print ("assets done, ready to proceed now")
def load_assets(self):
i = 0
while i < 10:
print("loading assets")
i += 1
sleep(1)
g = Game()
print("done")
This puts your asset loading to a thread and then enters loading_screen() in the main thread. This just prints messages to demonstrate they both run in parallel.

I can understand what problems the OP is having, a preferred solution is to use multiprocessing rather than multithreading. The given demo from the first answer uses time.sleep for simulating 'heavy loading' in the loading function, but when loading is actually going to take place, it will take a major amount of performance out of the CPU.
So it might happen that your loading animation is playing at 5-10 FPS when you expected 100+ FPS from it.
In this case, it is best to use multiprocessing to achieve the same. From what I have heard, it is a bit complicated to implement it, but it is quite possible. The loading function will then run as a separate process, not a separate thread from the same process.
Here is a small demo program to illustrate a loading screen using multiprocessing:
import multiprocessing
from multiprocessing.connection import Connection
def loading_screen(conn: Connection, stage):
clock = pygame.time.Clock()
fps = 60
c = 0
font = pygame.font.SysFont('consolas', 25)
while True:
screen.fill((0, 0, 50))
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
if e.type == pygame.KEYDOWN:
if e.key == pygame.K_ESCAPE:
return
if conn.poll(): # check if any object is available to be received or not
# this line is used to prevent waiting until the next object is received
c = conn.recv() # if available, receive the object
if c == 'end':
# process end condition checking
return
pygame.draw.rect(screen, 'white', (50, 50, c, 50))
screen.blit(font.render(f'{c} / {stage * 100} % [stage {stage}]', True, 'white'), (50, 150))
screen.blit(font.render('Check console for status', True, 'white'), (50, 200))
pygame.display.update()
clock.tick(fps)
pygame.display.set_caption(f'Multiprocessing Loading Screen [FPS = {int(clock.get_fps())}]')
def resource_load(conn: Connection, items):
# load resources
# for now it increments a counter and sends it via the connection object
# you can replace it for CPU intensive processes
# and pass the loaded objects to the connection object
c = 0
while True:
c += 1
if c > items:
conn.send('end') # signal end of loading current stage
return # returning will end the process automatically
conn.send(c) # send the counter value via the connection object
if __name__ == '__main__':
import pygame
pygame.init()
screen = pygame.display.set_mode((500, 300)) # initialize display
receiver, sender = multiprocessing.Pipe(duplex=False) # initialize sender and receiver connection objects [unidirectional]
print('Program to demonstrate a simple loading screen using multiprocessing')
print()
print('starting stages...')
multiprocessing.Process(target=resource_load, args=(sender, 100)).start()
print('loading stage 1')
loading_screen(receiver, 1)
multiprocessing.Process(target=resource_load, args=(sender, 200)).start()
print('loading stage 2')
loading_screen(receiver, 2)
multiprocessing.Process(target=resource_load, args=(sender, 300)).start()
print('loading stage 3')
loading_screen(receiver, 3)
input('finished loading all stages... press Enter to exit console') # press Enter inside the console to exit program
EDIT:
I have made a video on this if interested: https://www.youtube.com/watch?v=KWGDgPldPVo

Related

How to dynamically change self variables, parameters, args... in multiprocessing?

I don't know much of Python yet, but I'm trying to create an app that controls multiple streams of sound simultaneously (It has to do with binaural beats, noise and the brain). Given that I would like to control the volume and state of each of the tracks separately, I think that I need to use multiprocessing module. The first of those streams is a simple background music. I would like the user to pause it whenever he wants and I'm using pygame module.
import pygame
import time
import os
import multiprocessing
class play_music:
def __init__(self, path="", name=""):
self.state="" #pause, play, stop
self.name=name #name of the song
self.path=path #path of the song
def play_music(self):
path=self.path
pygame.init()
pygame.mixer.music.load(path)
pygame.mixer.music.play()
print (f"Playing {path}...")
while True:
if self.state=="pause":
pygame.mixer.music.pause()
self.state=""
while self.state=="":
if self.state == "continue":
pygame.mixer.music.unpause()
elif self.state=="stop"():
pygame.mixer.music.stop()
break
elif self.state=="stop":
pygame.mixer.music.stop()
break
def main():
task = ""
while not ( task == "meditacion" or task == "estudio"):
task = input ("Introduce que vas a hacer (meditacion, estudio): ").rstrip()
name ="non-existing track"
while not (os.path.exists(f"musica/{task}/{name}")):
name = input ("Introduce pista musical: ").rstrip()
path = f"musica/{task}/{name}"
print (f"Correct track. Path: {path}")
music = play_music()
music.path=path
music.name=name
p1 = multiprocessing.Process(target=music.play_music)
p1.start()
time.sleep(3) #for letting the process start correctly
while True:
music.state=input("pause, stop?: ")
if __name__=="__main__":
main()
This doesn't work. The state doesn't get modified whenever I input pause or stop. Any help is welcomed. Thank you so much in advance.
Since multiprocessing module creates separate Python processes, it is not possible to share variables in an easy way.
In case you would like to share variables yet still parallelize some of your computing, you should look into the built-in threading module.

Python threading crashing on asset load in PyGame

I have constructed some code to load assets into a character class. This works fine in a single thread, but I want to have a loading screen when the assets are loading.
I put the asset loading into a function, and tried to make it run in a separate thread. The issue is that it now hangs, and seemingly does not even execute the main thread.
I've kept an eye on the memory usage, and the memory is well-behaved (never goes above 500M out of 1976M, bits or bytes I'm not sure, it's whatever PyCharm reports).
import threading
import os
import pygame
from pygame.locals import *
def load_assets(screen: pygame.Surface, results: List):
print("thread: started to load assets in thread")
appearances1 = {} # some dictionary, Dict[str, Dict[str, str]]
default_outfit = "office_wear"
print("thread: instance of game class to be created")
game = Game("lmao", screen) # crashes somewhere here when running multithreaded
print("thread: game initialized") # this is never achieved
# rest of function not relevant
results.append(game)
def main():
# ====== INITIALIZE PYGAME ======
pygame.init()
pygame.font.init()
screen = pygame.display.set_mode((1024, 768))
clock = pygame.time.Clock()
loading = create_loading_screen(screen)
loading.display(screen)
pygame.display.flip()
# ====== START OTHER THREAD FOR LOADING ASSETS ======
results = []
x = threading.Thread(
target=load_assets, args=(screen, results), daemon=True
)
x.start()
while x.is_alive():
# never see this print statement!
print("x is fine") # want to blit loading screen here
# if I comment out the threading, and run the following instead, my game boots up just fine
# in under 3-4 seconds of waiting for the assets to load
# load_assets(screen, results)
game = results[0]
# other code here to render the game, no more threading after this point
while 1:
for event in pygame.event.get():
if event.type == QUIT:
return -1
game.main()
pygame.display.flip()
clock.tick(game.fps)
if __name__ == '__main__':
main()
You can see the print statements here (everything hangs and we don't even make it to the main thread!)
I figured out the issue. I feel really dumb now... The issue was my Game initializer was touching the pygame.display() which I already knew it shouldn't... I need a nap...

Stop Thread without closing GUI window

I am learning python on my own and my level is probably a poor excuse for a "script kiddie" as I kinda understand and mostly end up borrowing and mashing together different scripts till it does what I want. However this is the first time I'm trying to create a GUI for one of the scripts I have. I'm using PySimpleGUI and I've been able to understand it surprisingly well. All but one thing is working the way I want it to.
The issue is I want to stop a running daemon thread without exiting the GUI. If I get the stop button to work the GUI closes and if the GUI does not close it doesn't stop the thread. The issue is between lines '64-68'. I have tried a few things and just put a place holder on line '65' to remember that I was trying to keep the GUI ("Main Thread" in my head-speak) running. The script will run in this state but the 'Stop' button does not work.
Note: I put a lot of comments in my scripts so I remember what each part is, what it does and what I need to clean up. I don't know if this is a good practice if I plan on sharing a script. Also, if it matters, I use Visual Studio Code.
#!/usr/local/bin/python3
import PySimpleGUI as sg
import pyautogui
import queue
import threading
import time
import sys
from datetime import datetime
from idlelib import window
pyautogui.FAILSAFE = False
numMin = None
# ------------------ Thread ---------------------
def move_cursor(gui_queue):
if ((len(sys.argv)<2) or sys.argv[1].isalpha() or int(sys.argv[1])<1):
numMin = 3
else:
numMin = int(sys.argv[1])
while(True):
x=0
while(x<numMin):
time.sleep(5) # Set short for debugging (will set to '60' later)
x+=1
for i in range(0,50):
pyautogui.moveTo(0,i*4)
pyautogui.moveTo(1,1)
for i in range(0,3):
pyautogui.press("shift")
print("Movement made at {}".format(datetime.now().time()))
# --------------------- GUI ---------------------
def the_gui():
sg.theme('LightGrey1') # Add a touch of color
gui_queue = queue.Queue() # Used to communicate between GUI and thread
layout = [ [sg.Text('Execution Log')],
[sg.Output(size=(30, 6))],
[sg.Button('Start'), sg.Button('Stop'), sg.Button('Click Me'), sg.Button('Close')] ]
window = sg.Window('Stay Available', layout)
# -------------- EVENT LOOP ---------------------
# Event Loop to process "events"
while True:
event, values = window.read(timeout=100)
if event in (None,'Close'):
break
elif event.startswith('Start'): # Start button event
try:
print('Starting "Stay Available" app')
threading.Thread(target=move_cursor,
args=(gui_queue,), daemon=True).start()
except queue.Empty:
print('App did not run')
elif event.startswith('Stop'): # Stop button event
try:
print('Stopping "Stay Available" app')
threading.main_thread # To remind me I want to go back to the original state
except queue.Empty:
print('App did not stop')
elif event == 'Click Me': # To see if GUI is responding (will be removed later)
print('Your GUI is alive and well')
window.close(); del window
if __name__ == '__main__':
gui_queue = queue.Queue() # Not sure if it goes here or where it is above
the_gui()
print('Exiting Program')
From this answer: create the class stoppable_thread.
Then: store the threads on a global variable:
# [...]
# store the threads on a global variable or somewhere
all_threads = []
# Create the function that will send events to the ui loop
def start_reading(window, sudo_password = ""):
While True:
window.write_event_value('-THREAD-', 'event')
time.sleep(.5)
# Create start and stop threads function
def start_thread(window):
t1 = Stoppable_Thread(target=start_reading, args=(window,), daemon=True)
t1.start()
all_threads.append(t1)
def stop_all_threads():
for thread in all_threads:
thread.terminate()
Finally, on the main window loop, handle the events that start, stop or get information from the thread.

Bizarre behaviour of Pygame mixed with Multiprocessing

I am working on a small project including Raspberry Pi camera. I am using multiprocessing on the PC to perform various tasks and one of them is displaying images in a Pygame window. I have manager to minimize the code suffering the problem. This is the main function, which just creates a Game process, runs it and waits for it to end:
# main.py
from multiprocessing import Manager
import game
with Manager() as manager:
namespace = manager.Namespace()
namespace.done = False
jobs = [
game.Game(namespace),
]
for job in jobs:
job.start()
for job in jobs:
job.join()
Below is the code of the Game class. The state is shared because there will be other processes checking if the program has finished.
# game.py
import multiprocessing
import pygame
import time
import sys
class Game(multiprocessing.Process):
def __init__(self, the_state):
super(Game, self).__init__()
pygame.init()
self.screen = pygame.display.set_mode((100, 100))
self.state = the_state
def handle_key_events(self):
""" Wrapper for handling key events """
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.state.done = True
return
def run(self):
while True:
self.handle_key_events()
if self.state.done:
print('exiting PyGame thread')
pygame.display.quit()
pygame.quit()
return
The behaviour right now is that it doesn't end on clicking the Pygame's exit button. However, if I comment out the pygame.quit() line from Game's run method, it gives me the following message:
exiting PyGame thread
XIO: fatal IO error 11 (Resource temporarily unavailable) on X server ":0"
after 120 requests (120 known processed) with 1 events remaining.
I tried googling that error but little success. I can't shake off the feeling that I'm attempting something I shouldn't be, considering that probably the Pygame's window is a process on its own.
I would appreciate some insight and recommendations on where to read up
As Blckknght suggested, the issue was that the parent process (main.py) initiated PyGame's window, while the child process (game.py) attempted to quit it. It can be resolved by moving this part:
pygame.init()
self.screen = pygame.display.set_mode((100, 100))
into Game's run method

How can I thread this code so late in development?

I have been making a GUI for a genetic algorithm I am working on and I made the mistake of leaving the threading so late simply because I did not (and still don't) know how to do it. So essentially when the start button is clicked the function 'run' starts the whole infinite loop process which actually happens in generation_loop. Each generation the loop checks to see if it should still be running. The idea is that if the stop or pause button has been clicked it will stop looping (with the stop button all the data is cleared with the pause button it remains and the unpause button just sets running to True and calls generation_loop)
So I need to work out a way to make my GUI responsive while generation_loop is running. Here is my code, I tried to minimise it but I am unsure what is important information for threading:
class Window(main_window, QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
main_window.__init__(self)
self.setupUi(self)
self.scene = QGraphicsScene()
self.im_view.setScene(self.scene)
self.setWindowTitle('Fantasy Generator')
self.running = False
self.first_run = True
self.im = Image.new('RGBA', (400, 400), (0, 0, 0, 255))
self.saved_gens = deque([('A', self.im, self.im, self.im)])
self.set_save_amount(self.sb_saveamt.value())
self.population = []
self.btn_exit.clicked.connect(self.close)
self.actionQuit.triggered.connect(self.close)
self.btn_pauser.clicked.connect(self.pause_button)
self.sb_saveamt.valueChanged[int].connect(self.set_save_amount)
self.btn_restart.clicked.connect(self.start_button)
self.btn_loadimage.clicked.connect(self.get_image)
self.actionLoad_Image.triggered.connect(self.get_image)
self.gen_sldr.valueChanged[int].connect(self.display_gen)
self.cb_display.currentIndexChanged.connect(self.change_quality)
self.has_image = True
self.display_gen(0)
def get_image(self):
pass
# To save you time I removed the code here. It just sets self.im using a file dialog basically
def set_save_amount(self, amt):
if amt == -1:
self.saved_gens = deque(self.saved_gens)
else:
self.saved_gens = deque(self.saved_gens, amt + 1)
def pause_button(self):
if self.first_run:
self.run()
elif self.running:
self.running = False
self.btn_pauser.setText('Resume Execution')
# pause stuff goes here
else:
self.running = True
self.btn_pauser.setText('Pause Execution')
self.generation_loop()
# resume from pause stuff goes here
def start_button(self):
if self.first_run:
self.run()
else:
self.end()
# The run function should start the actual process
def run(self):
self.btn_restart.setText('End')
self.btn_pauser.setText('Pause Execution')
self.first_run = False
self.running = True
settings = dict(ind_per_gen=self.sb_ipg.value(), shapes_per_im=self.sb_spi.value(),
complexity=self.sb_complexity.value(), mut_rate=self.sb_mutation.value(),
cross_chance=self.sb_cross.value(), seed=self.sb_seed.value())
self.population = Population(self.im, **settings)
self.generation_loop()
# This is the loop I want to be able to exit out of using buttons
def generation_loop(self):
while self.running:
if self.first_run:
break
self.add_generation_data(self.population.next_gen())
def end(self):
self.btn_restart.setText('Start')
self.btn_pauser.setText('Start Execution')
self.first_run = True
self.running = False
self.saved_gens = deque([('A', self.im, self.im, self.im)])
self.set_save_amount()
self.display_gen(0)
def add_generation_data(self, data):
self.saved_gens.append(data)
self.gen_sldr.setMaximum(len(self.saved_gens) - 1)
self.gen_sldr.setValue(len(self.saved_gens) - 1)
self.display_gen(data[0] + 1)
def change_quality(self):
self.display_gen(self.gen_sldr.value())
def resizeEvent(self, e):
if self.has_image:
self.im_view.fitInView(QRectF(0, 0, self.width, self.height), Qt.KeepAspectRatio)
self.scene.update()
def display_image(self, image):
self.scene.clear()
if image.mode != 'RGBA':
image = image.convert('RGBA')
self.width, self.height = image.size
qim = ImageQt.ImageQt(image)
pixmap = QPixmap.fromImage(qim)
self.scene.addPixmap(pixmap)
self.im_view.fitInView(QRectF(0, 0, self.width, self.height), Qt.KeepAspectRatio)
self.scene.update()
def display_gen(self, index):
self.lcd_cur_gen.display(self.saved_gens[index][0])
if self.cb_display.currentIndex() == 0:
self.display_image(self.saved_gens[index][1])
elif self.cb_display.currentIndex() == 1:
self.display_image(self.saved_gens[index][2])
else:
self.display_image(self.saved_gens[index][3])
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
EDIT: I also just found at that I can't even change the graphics view from within the generation_loop but it works and changes if I limit the loop
In order to move your long running code to a thread, you need to first identify which parts of the long running code interact with the GUI and which parts don't. The key reason for this is that interacting with the GUI from a secondary thread is forbidden, and will lead to segfaults.
It looks like self.population.next_gen() is the long running bit of the code and doesn't interact with the GUI (although what this does is not provided so I can't be sure) while self.add_generation_data(...) updates the GUI which should be reasonably fast.
As such, this makes it reasonably simple to separate, which I'll show below.
Now, about threads. Python provides threads through the threading module (as the other answers show), however these not are recommended for use with a PyQt application if you want your thread to have any relation to the GUI (see here). PyQt also provides threading via the QThread object, which integrates support for sending and receiving Qt signals (which are thread safe). In short, the QThread has a separate event loop, and processes signals received asynchronously to the main thread, thus leaving the event loop in the main thread to process GUI events (like button clicks).
Typically you create a new class that inherits from QObject, instantiate it and move it to a QThread. Slots (aka methods) in the object that are triggered by a signal emission, then run in the thread.
So you'll want to do something like this
class MyWorker(QObject):
done = pyqtSignal(object) # you may need to update "object" to the type returned by Population.next_gen()
def __init__(self, settings):
# create the population object with whatever settings you need
# Note that this method runs in the parent thread as you have
# yet to move the object to a new thread. It shouldn't cause any
# problems, but may depend on what the Population class is/does.
# TODO: I've removed the reference to an image here...
#it may or may not be thread safe. I can't tell from your code.
self.population = Population(..., settings)
#pyqtSlot()
def next_gen(self):
new_gen = self.population.next_gen()
self.done.emit(new_gen)
class Window(....):
make_next_generation = pyqtSignal()
....
def run(self):
self.btn_restart.setText('End')
self.btn_pauser.setText('Pause Execution')
self.first_run = False
self.running = True
settings = dict(ind_per_gen=self.sb_ipg.value(), shapes_per_im=self.sb_spi.value(),
complexity=self.sb_complexity.value(), mut_rate=self.sb_mutation.value(),
cross_chance=self.sb_cross.value(), seed=self.sb_seed.value())
self.setupThread(settings)
def setupThread(self, settings):
self.thread = QThread()
self.worker = MyWorker(settings)
self.worker.moveToThread(self.thread)
# connect a signal in the main thread, to a slot in the worker.
# whenever you emit the signal, a new generation will be generated
# in the worker thread
self.make_next_generation.connect(self.worker.next_gen)
# connect the signal from the worker, to a slot in the main thread.
# This allows you to update the GUI when a generation has been made
self.worker.done.connect(self.process_generation)
# Start thread
self.thread.start()
# emit the signal to start the process!
self.make_next_generation.emit()
def process_generation(new_gen):
# run the GUI side of the code
# ignore the new generation if the "end" button was clicked
if not self.first_run:
self.add_generation_data(new_gen)
if self.running:
# make another generation in the thread!
self.make_next_generation.emit()
def pause_button(self):
if self.first_run:
self.run()
elif self.running:
self.running = False
self.btn_pauser.setText('Resume Execution')
# pause stuff goes here
else:
self.running = True
self.btn_pauser.setText('Pause Execution')
# make another generation in the thread!
self.make_next_generation.emit()
Things to note:
I haven't included all of your code in my answer. Merge as appropriate.
I'm unsure what self.im is. It's passed to Population so there might be some thread unsafe behaviour in your code that I can't see. I've left it to you to fix
I'm familiar with PyQt4, not PyQt5, so there is a possibility some things I've done don't work quite right. It should be easy for you to work out what to change from any error messages that are raised.
It's a bit messy recreating the thread and worker each time it is started from scratch. You might want to consider moving the instantiation of Population to a method in the worker (one that isn't __init__ and invoking it each time you want to start from scratch (in the same way we trigger a new generation). This would allow you to move pretty much all of setupThread to the Window.__init__ method and then when the start button was clicked, you'd just emit a signal to recreate Population followed by one to make the first generation.
You can use Threading events here.
from threading import Thread, Event
Once you detect the button click,
class MyThread(Thread):
def __init__(self, the_function, <any input param you want to provide>):
Thread.__init__(self)
self.stop_event = Event()
self.exec_func = the_function
def set_stop_flag(self, value):
if value:
self.stop_event.set()
else:
self.stop_event.clear()
def run(self):
while True:
try:
if not self.stop_event.is_set()
self.exec_func()
else:
break # once the event is set, you can break which will kill this thread.
# To stop busy waiting you can make this thread sleep for some seconds after each cycle.
import time
time.sleep(10) # 10 seconds wait before the next cycle.
except Exception, excep:
print "Got exception > ", str(excep)
Now in your code you embed this code piece and keep a reference for this thread.
Let's say
self.my_thread = MyThread(self.function_to_perform, <blabla>)
self.my_thread.setDaemon(True) # So that you don't have to worry about it when the Main process dies!
self.my_thread.start()
Now once you get a STOP button click event you call
self.my_thread.set_stop_flag(True) # Bingo! Your thread shall quit.

Categories