pyagme screen not working with multithreading

2019-08-23 09:33发布

I am using python 2.7.14 and am currently trying to draw to 2 sides of a pygame screen simultaneously using the multiproccesing module(2 threads are calling functions from a single pygame screen) but everytime I call a function from screen(like screen.get_width() for example) the following error is raised:

Process Process-1:
Traceback (most recent call last):
  File "C:\Python27\lib\multiprocessing\process.py", line 267, in _bootstrap
    self.run()
  File "C:\Python27\lib\multiprocessing\process.py", line 114, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Python27\multiplayer.py", line 9, in single_core_game
    print screen.get_width()
error: display Surface quit

I know writing from threads isn't the most elegant way so I'll be glad to hear alternatives.

Thanks!

1条回答
小情绪 Triste *
2楼-- · 2019-08-23 10:00

As @Fredrik comments, you probably want to use a thread, not a separate process. Threads share access to memory & variables etc., whereas separate processes have a separate copy from the point the child process was started.

So if I may re-phrase your question:

How do I draw to the pygame screen from multiple threads?

And to which the short-answer is: "You don't".

Generally, with event driven, window-based desktops, programs have a single thread that manages interaction with the user, including handling input and output to the screen. Sure you might be able to call screen.blit(...) from multiple threads because of the Python GIL, but it's not a good design path to be walking.

But! That is not to say other threads and processes cannot create content for the display, but then hand it off to the main handler thread for that final blit to the screen, so how?

Separate python processes can communicate with each other with Client and Listener pipes (this gets around the GIL too), and as mentioned before, separate threads can just share memory.

Here's a kludgey piece of code that renders a background image to an off-screen surface, and then posts an event to the main thread when each new update is ready. Obviously it's a trivial use of a thread, but if there was a more time-consuming update process, it would fit better.

The thread function initially creates a pygame Surface, making it look like an 8-bit rendition of the inky blackness of space. Then forever-more pans across the image, sending a copy as a Pygame.Event to the main thread via the event queue. The main thread sees this event, and updates it's background image.

The result is kind-of jerky, but this is because I put the thread to sleep for 500ms on each iteration to slow it down a bit.

import threading
import pygame
import random
import time
import sys

# Window size
WINDOW_WIDTH=400
WINDOW_HEIGHT=400
DARK_GREY   = (  50,  50,  50 )
SPACE_BLACK = (   0,   0,  77 )
STAR_WHITE  = ( 255, 252, 216 )


### Thread that paints a background image to an off-screen surface
### then posts an event to the main loop when the image is ready
### for displaying.
class BackgroundDrawThread( threading.Thread ):
    def __init__( self ):
        threading.Thread.__init__(self)
        self.daemon         = True # exit with parent
        self.sleep_event    = threading.Event()
        self.ofscreen_block = pygame.Surface( ( WINDOW_WIDTH, WINDOW_HEIGHT ) )
        self.pan_pixels     = 5

    def makeSpace( self ):
        """ Make a starry background """
        # Inky blackness of space
        self.ofscreen_block.fill( SPACE_BLACK )
        # With some (budget-minded) stars
        for i in range( 80 ):
            random_pixel = ( random.randrange( WINDOW_WIDTH ), random.randrange( WINDOW_HEIGHT ) )
            self.ofscreen_block.set_at( random_pixel, STAR_WHITE )

    def panSpace( self ):
        """ Shift space left, by some pixels, wrapping the image """
        rect_to_move = [0, 0, self.pan_pixels, WINDOW_HEIGHT-1]
        lost_part = self.ofscreen_block.subsurface( rect_to_move ).copy()
        self.ofscreen_block.scroll( dx=-self.pan_pixels, dy=0)
        self.ofscreen_block.blit( lost_part, ( WINDOW_WIDTH-1-self.pan_pixels,0) )

    def run( self ):
        """ Do Forever (or until interuppted) """
        # Make the space backdrop
        self.makeSpace()
        while ( True ):
            if ( True == self.sleep_event.wait( timeout=0.5 ) ):
                break # sleep was interrupted by self.stop()
            else:
                # Rotate space a bit
                self.panSpace()

                # Message the main-thread that space has been panned.
                new_event_args = { "move": self.pan_pixels, "bitmap": self.ofscreen_block.copy() }
                new_event = pygame.event.Event( pygame.USEREVENT + 1, new_event_args )
                pygame.event.post( new_event )

    def stop( self ):
        self.sleep_event.set() # interrupt the wait
        self.join()



### MAIN
pygame.init()
pygame.display.set_caption("Threaded Paint")
WINDOW  = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE )

# Start the render-my-background thread
thread1 = BackgroundDrawThread()
thread1.start()
background = None

# Main paint / update / event loop
done = False
clock = pygame.time.Clock()
while ( not done ):

    # Handle Events
    for event in pygame.event.get():
        if ( event.type == pygame.QUIT ):
            done = True

        elif ( event.type == pygame.USEREVENT + 1 ):
            background = event.bitmap   
            print( "Space folded by %d pixels" % ( event.move ) )

    # Paint the window
    if ( background != None ): # wait for the first backgroun to be ready message
        WINDOW.blit( background, (0,0) )
    pygame.display.flip()

    # Max FPS
    clock.tick_busy_loop(60)

thread1.stop()
pygame.quit()

pan_space.gif

Hmm, looking at the animation, it seems like I have an off-by-1 bug in the folding of space. I guess the code needs more spice.

查看更多
登录 后发表回答