Multithreading Python turtle with recursion
I made a Python turtle program that recursively generated a fractal tree, but, since it often took a few hours to fully draw, I wanted to try to use multithreading to have multiple turtles working together.
I was able to get two turtles moving at the same time, but, in this much more complex situation, everything seemed to fall apart. I've tried this many different ways, and thought this final solution would be the one, but it just throws a bunch of errors.
Here's my code:
import turtle
import threading
from queue import Queue
class Location:
def __init__(self, xpos=0, ypos=0, heading=90):
self.xpos = xpos
self.ypos = ypos
self.heading = heading
def getx(self):
return self.xpos
def gety(self):
return self.ypos
def geth(self):
return self.heading
class Turtle(turtle.Turtle):
def tolocation(self, location):
self.penup()
self.setx(location.getx())
self.sety(location.gety())
self.setheading(location.geth())
self.pendown()
def get_location(self):
return Location(self.xcor(), self.ycor(), self.heading())
def draw_tree(self, startpos=Location(), size=100):
tm.q.put(self.tolocation(startpos))
for _ in range(size):
tm.q.put(self.forward(1))
for _ in range(45):
tm.q.put(self.right(1))
t2 = Turtle()
t2.speed(0)
tm.new_thread(t2.draw_tree, self.get_location(), size / 2)
for _ in range(90):
tm.q.put(self.left(1))
tm.new_thread(self.draw_tree, self.get_location(), size / 2)
class ThreadManager:
def __init__(self):
self.q = Queue()
self.threads = []
def new_thread(self, func, *args):
self.threads.append(threading.Thread(target=func, args=(args,)))
self.threads[-1].daemon = True
self.threads[-1].start()
def process_queue(self, scr):
while not self.q.empty():
(self.q.get())(1)
if threading.active_count() > 1:
scr.ontimer(self.process_queue(scr), 100)
tm = ThreadManager()
scr = turtle.Screen()
t1 = Turtle()
t1.speed(0)
tm.new_thread(t1.draw_tree)
tm.process_queue(scr)
scr.exitonclick()
Can anyone give me an idea of where I went wrong here? The error messages say something along the lines of the recursion going too deep when process_queue calls itself. Am I using scr.ontimer()
wrong?
There are several problems with your code:
draw_tree()
doesn't have a base case to stop it's (conceptual) recursion, it just keeps creating new threads.
Each call to draw_tree()
divides size
in half using floating division so range(size)
will fail as range()
can only take ints.
Your calls to self.get_location()
are not valid as it tells you where the turtle is, not where it will be once the main thread finishes processing outstanding graphics commands. You have to compute where you will be, not look where you are.
This call, tm.q.put(self.tolocation(startpos))
isn't valid -- you either need to do tm.q.put(self.tolocation, startpos)
or call tm.q.put()
on each command inside self.tolocation()
.
You can't create new turtles anywhere but the main thread, as they invoke tkinter on creation and that'll be on the wrong (not main) thread. In my rework below, I simply preallocate them.
args=(args,)
is incorrect -- should be args=args
as args
is already in the correct format.
You don't need to create two new threads at each branching point, just one. The new turtle goes one way, the old turtle continues on the other.
Below is my rework of your code to address the above and other issues:
import math
import threading
from queue import Queue
from turtle import Turtle, Screen
class Location:
def __init__(self, xpos=0, ypos=0, heading=90):
self.xpos = xpos
self.ypos = ypos
self.heading = heading
def clone(self):
return Location(self.xpos, self.ypos, self.heading)
class Terrapin(Turtle):
def tolocation(self, location):
tm.q.put((self.penup,))
tm.q.put((self.setx, location.xpos))
tm.q.put((self.sety, location.ypos))
tm.q.put((self.setheading, location.heading))
tm.q.put((self.pendown,))
def draw_tree(self, startpos, size=100):
if size < 1:
return
self.tolocation(startpos)
tm.q.put((self.forward, size))
angle = math.radians(startpos.heading)
startpos.xpos += size * math.cos(angle)
startpos.ypos += size * math.sin(angle)
tm.q.put((self.right, 45))
startpos.heading -= 45
tm.new_thread(pond.get().draw_tree, startpos.clone(), size / 2)
tm.q.put((self.left, 90))
startpos.heading += 90
self.draw_tree(startpos, size / 2)
pond.put(self) # finished with this turtle, return it to pond
class ThreadManager:
def __init__(self):
self.q = Queue()
self.threads = Queue()
def new_thread(self, method, *arguments):
thread = threading.Thread(target=method, args=arguments)
thread.daemon = True
thread.start()
self.threads.put(thread)
def process_queue(self):
while not self.q.empty():
command, *arguments = self.q.get()
command(*arguments)
if threading.active_count() > 1:
screen.ontimer(self.process_queue, 100)
screen = Screen()
# Allocate all the turtles we'll need ahead as turtle creation inside
# threads calls into Tk which fails if not running in the main thread
pond = Queue()
for _ in range(100):
turtle = Terrapin(visible=False)
turtle.speed('fastest')
pond.put(turtle)
tm = ThreadManager()
tm.new_thread(pond.get().draw_tree, Location())
tm.process_queue()
screen.exitonclick()
if instead of large steps:
tm.q.put((self.right, 45))
you want to break down graphic commands into small steps:
for _ in range(45):
tm.q.put((self.right, 1))
that's OK, I just wanted to get the code to run. You need to figure out if this gains you anything.
链接地址: http://www.djcxy.com/p/80716.html上一篇: 用于Haskell id函数
下一篇: 带递归的多线程Python乌龟