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乌龟