Multi-threading while using john zelles graphics module (Python)

31 views Asked by At

I have been using the John Zelle graphics module for some time now, however this is my first time attempting to use the threading module. I am trying to make multiple objects (circles) that have different starting positions move along my screen simultaneously. This code will also contain recursion and jumping to multiple functions in its final product.

Here is an example of what I am trying to do:

from graphics import *
from random import randint
import time
import threading

class Particle:
    
    def __init__(self):
        self.xpos   = randint(0,50)
        self.ypos   = randint(0,50)
        
        self.graphic = Circle(Point(self.xpos,self.ypos),5)    
        self.graphic.draw(window)
        
    def move(self):
        for step in range(5):
            self.xpos += 1
            self.ypos += 1
            self.graphic.move(self.xpos,self.ypos)
            time.sleep(0.5)
            
window = GraphWin("window",500,500)

threading.Thread(target=Particle()).start()
threading.Thread(target=Particle()).start()

After different attempts of trying to use threading, I contnued to get two different errors.

This (from the code above):

Exception in thread Thread-1:
Traceback (most recent call last):
Thread-2  File "/usr/lib/python3.11/threading.py", line 1038, in _bootstrap_inner
:
    Traceback (most recent call last):
  File "/usr/lib/python3.11/threading.py", line 1038, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.11/threading.py", line 975, in run
    self._target(*self._args, **self._kwargs)
TypeError: 'Particle' object is not callable
self.run()
  File "/usr/lib/python3.11/threading.py", line 975, in run
    self._target(*self._args, **self._kwargs)
TypeError: 'Particle' object is not callable

and also: "RuntimeError: main thread is not in main loop"

I have tried many suggestions from stack overflow and other websites such as:

  • Setting my threads to 'daemon = True'
  • using 'plt.switch_backend('agg')'
  • making a separate thread class
  • giving my thread a handle and then using .start on said handle

note: if you are certain that these methods should work please show me how it should be done and I am new to using the thread module.

thanks for any help!

2

There are 2 answers

0
Vinay Davda On

I tried your code,it need some fixes:

  • in threading.Thread target you are passing class instance directly while it accepts methods
  • you can pass Particle.__init__() or Particle().move() as action
  • or you can create object using particle1 = Particle() class and then use it's methods like target=particle1.move()
  • If you use daemon=True it means newly created thread will run in background and main Thread doesn't have to wait till it completes it's process
  • If you call join() on thread's instance like thread1.join(), main thread will wait till that perticular thread completes it's targeted action (daemon=True or False doesn't matter in this case)

Here I am giving you some corrected version of code:

# ... same other code

threading.Thread(target=Particle().__init__()).start()
threading.Thread(target=Particle().__init__()).start()

or

# ... same other code

threading.Thread(target=Particle().move()).start()
threading.Thread(target=Particle().move()).start()

or

# ... same other code

particle1 = Particle()
particle2 = Particle()

threading.Thread(target=particle1.move()).start()
threading.Thread(target=particle2.move()).start()

or

# ... same upper code

particle1 = Particle()
particle2 = Particle()

thread1 = threading.Thread(target=particle1.move()).start()
thread2 = threading.Thread(target=particle2.move()).start()

thread1.join()
thread2.join()

In last snippet, you your window will be opened till your move function completes execution or you close it manually.

If you want to dig deeper in Pytohn Threading, check this guide: https://superfastpython.com/threading-in-python/

0
cdlane On

It's rough but I believe it gives you a working starting point:

from queue import Queue
from random import randint
from threading import Thread
from time import sleep
from graphics import *

class Particle:
    def __init__(self):
        xpos = randint(0, 50)
        ypos = randint(0, 50)

        self.graphic = Circle(Point(xpos, ypos), 5)
        self.graphic.draw(window)

    def move(self):
        while True:
            graphic_commands.put((self.graphic.move, randint(1, 5), randint(1, 5)))
            sleep(0.25)  # sleep *this* thread

def process_queue():
    while not graphic_commands.empty():
        graphic, *arguments = graphic_commands.get()
        graphic(*arguments)

    window.after(100, process_queue)

graphic_commands = Queue(1)  # size = ~ number of hardware threads you have - 1

window = GraphWin("window", 500, 500)

process_queue()

Thread(target=Particle().move, daemon=True).start()
Thread(target=Particle().move, daemon=True).start()
Thread(target=Particle().move, daemon=True).start()

window.getMouse()
window.close()

It's based on a similar solution I did for turtle, but with additional twists for graphics.py. The (thread safe) queue is used to make sure all the threads' drawing requests are executed in the main thread for tkinter.