Search code examples
pythonmultithreadingpython-turtle

Turtle threaded parallel drawing


I am currently trying to make a sierpinski triangle which uses a recursive function to create threads that draw individual triangles simultaneously using the turtle library(cant change that). The thing is it keeps telling me that RuntimeError: main thread is not in main loop.

Here is the code I use for thread creating and execution, as well as some of my attempts at fixing it


def triangle_thread(x, y, size, invert=False):
    global turtles
    turtles.append(turtle.Turtle("turtle"))
    turtles[-1].speed("fastest")
    t = threading.Thread(target=partial(triangle, x, y, size, turtles[-1], invert=invert))
    t.daemon = True
    t.start()

This is supposed to create and start a new thread that draws a triangle It apparently works.

I tried multiple things, as well as some queue trickery but it wouldnt draw them simultaneously.

here is my latest attempt at fixing it :

thread_sierpinski_recursive = threading.Thread(target=partial(sierpinski, -700, -500, 3, 1000))
thread_sierpinski_recursive.start()
turtle.mainloop()

I tried to run the entire sierpinski triangle generation in a separate thread so the main thread would be running turtle.mainloop

this works with sierpinski with up to 4 generations, but as soon as you try more it returns the same error:

sierpinski(-700, -500, 3, 1000)
turtle.mainloop()

here is the whole code I use, after trying cdlane's solution :

import turtle
import math
from functools import partial
import threading

threads = []
turtle.speed("fastest")


def triangle(x, y, size, t, invert=False):
    y_offset = size*(math.sqrt(3)/2)
    t.hideturtle()
    t.penup()
    partial(t.goto, x, y)()
    t.pendown()
    partial(t.goto, x+size, y)()
    partial(t.goto, x+size/2, y-y_offset if invert else y+y_offset)()
    partial(t.goto, x, y)()


def inner_sierpinski(x, y, size, iteration, thread_in):
    global threads
    height = size * (math.sqrt(3) / 2)
    triangle(x + (size / 2), y + height, size, thread_in.Turtle, invert=True)
    if iteration > 3:
        for nx, ny in ((x, y), (x+(size/2), y+height), (x+size, y)):
            threads.append(threading.Thread(target=partial(sierpinski, x, y, iteration-1, size, is_first=False)))
            threads[-1].Turtle = turtle.Turtle("turtle", visible=False)
            threads[-1].Turtle.speed("fastest")
    elif iteration > 1:
        for nx, ny in ((x, y), (x+(size/2), y+height), (x+size, y)):
            sierpinski(nx, ny, iteration-1, size, is_first=False)


#this exists for the sole purpose of drawing the outer triangle
#this function calls inner_sierpinski, whoch calls this thrice.
def sierpinski(x, y, iterations, total_size, is_first=True):
    global threads
    if is_first:
        triangle(x, y, total_size, turtle)
        threading.main_thread().Turtle = turtle
        thread_out = threading.main_thread()
    else:
        thread_out = threads[-1]
    inner_sierpinski(x, y, total_size/2, iterations, thread_out)


sierpinski(-700, -500, 6, 1000)
for thread in threads:
    thread.start()
turtle.mainloop()

Solution

  • I would need to see more of your code to understand specifically, but generally, drawing an individual triangle seems too small a step to thread as the overhead overwhelms the process. If each triangle was a complex calculation that took significant time, that would be fine. Similarly, if you break up your Sierpinski triangle into large sections that could be drawn in parallel, that would also seem reasonable.

    Below is code I just pulled together from my other examples that uses threading to recursively draw the three large components of a Koch snowflake in parallel:

    from queue import Queue
    from functools import partial
    from turtle import Screen, Turtle
    from threading import Thread, active_count
    
    STEPS = 3
    LENGTH = 300
    
    def koch_curve(turtle, steps, length):
        if steps == 0:
            actions.put((turtle.forward, length))
        else:
            for angle in [60, -120, 60, 0]:
                koch_curve(turtle, steps - 1, length / 3)
                actions.put((turtle.left, angle))
    
    def process_queue():
        while not actions.empty():
            action, *arguments = actions.get()
            action(*arguments)
    
        if active_count() > 1:
            screen.ontimer(process_queue, 1)
    
    screen = Screen()
    actions = Queue(1)  # size = number of hardware threads you have - 1
    
    turtle1 = Turtle('turtle')
    turtle1.hideturtle()
    turtle1.speed('fastest')
    turtle1.color('red')
    turtle1.penup()
    turtle1.right(30)
    turtle1.backward(3**0.5 * LENGTH / 3)
    turtle1.left(30)
    turtle1.pendown()
    
    turtle2 = turtle1.clone()
    turtle2.color('green')
    turtle2.penup()
    turtle2.forward(LENGTH)
    turtle2.right(120)
    turtle2.pendown()
    
    turtle3 = turtle1.clone()
    turtle3.speed('fastest')
    turtle3.color('blue')
    turtle3.penup()
    turtle3.right(240)
    turtle3.backward(LENGTH)
    turtle3.pendown()
    
    thread1 = Thread(target=partial(koch_curve, turtle1, STEPS, LENGTH))
    thread1.daemon = True  # thread dies when main thread (only non-daemon thread) exits.
    thread1.start()
    
    thread2 = Thread(target=partial(koch_curve, turtle2, STEPS, LENGTH))
    thread2.daemon = True
    thread2.start()
    
    thread3 = Thread(target=partial(koch_curve, turtle3, STEPS, LENGTH))
    thread3.daemon = True
    thread3.start()
    
    process_queue()
    
    screen.exitonclick()
    

    Since the amount of drawing is the same (and the limiting factor on the speed) I don't expect to gain any performance over simply drawing the fractal the usual way with the turtle speed turned up or tracing turned off. It's just for visual effect.

    enter image description here

    1/16 Update: Reviewing your attempt to incorporate my example into your code it's clear you don't understand what the role of partial() is, nor, for that matter, the role of the global statement in Python. And you're missing a key concept: all turtle graphics commands must happen in the main thread, not any of the ones you create. (I.e. turtle's underpinning code is not thread safe.)

    Let's begin by turning your code back into a non-threaded implemenation of the Sierpinski triangle. But with a slight change in that we will make the triangles on the corners of the top level triangle three different colors, to show we know when those major segments begin:

    from turtle import Screen, Turtle
    
    def triangle(x, y, size, turtle, invert=False):
        y_offset = size * 3**0.5/2
    
        turtle.penup()
        turtle.goto(x, y)
        turtle.pendown()
        turtle.goto(x + size, y)
        turtle.goto(x + size/2, y + (-y_offset if invert else y_offset))
        turtle.goto(x, y)
    
    def inner_sierpinski(x, y, size, turtle, iterations, iteration):
        height = size * 3**0.5/2
    
        triangle(x + size/2, y + height, size, turtle, invert=True)
    
        if iteration:
            for nx, ny in ((x, y), (x + size/2, y + height), (x + size, y)):
                sierpinski(nx, ny, size, turtle, iterations, iteration-1)
    
    def sierpinski(x, y, size, turtle, iterations, iteration=-1):
        if iteration < 0:
            triangle(x, y, size, turtle)
            iteration = iterations
        elif iteration == iterations - 1:
            turtle.color(colors.pop())
        
        inner_sierpinski(x, y, size/2, turtle, iterations, iteration)
    
    colors = ['black', 'red', 'green', 'blue']
    
    screen = Screen()
    
    turtle = Turtle(visible=False)
    turtle.speed('fastest')
    turtle.color(colors.pop(0))
    
    sierpinski(-350, -250, 700, turtle, 6)
    
    screen.exitonclick()
    

    Having done that, let's now incorporate the threading from my Koch snowflake example:

    from queue import Queue
    from functools import partial
    from turtle import Screen, Turtle
    from threading import Thread, active_count
    
    def triangle(x, y, size, turtle, invert=False):
        y_offset = size * 3**0.5/2
    
        actions.put((turtle.penup,))
        actions.put((turtle.goto, x, y))
        actions.put((turtle.pendown,))
        actions.put((turtle.goto, x + size, y))
        actions.put((turtle.goto, x + size/2, y + (-y_offset if invert else y_offset)))
        actions.put((turtle.goto, x, y))
    
    def inner_sierpinski(x, y, size, turtle, iterations, iteration):
        # Surprisingly, this doesn't change from my
        # previous example so just copy it over!
    
    def sierpinski(x, y, size, turtle, iterations, iteration=-1):
        if iteration < 0:
            triangle(x, y, size, turtle)
            iteration = iterations
    
        if iteration == iterations - 1:
            turtle = turtles.get()
    
            thread = Thread(target=partial(inner_sierpinski, x, y, size/2, turtle, iterations, iteration))
            thread.daemon = True
            thread.start()
        else:
            inner_sierpinski(x, y, size/2, turtle, iterations, iteration)
    
    def process_queue():
        # Copy this function from my Koch snowflake example
    
    COLORS = ['black', 'red', 'green', 'blue']
    turtles = Queue(len(COLORS))
    
    screen = Screen()
    
    for color in COLORS:
        turtle = Turtle(visible=False)
        turtle.speed('fastest')
        turtle.color(color)
    
        turtles.put(turtle)
    
    actions = Queue(1)
    
    thread = Thread(target=partial(sierpinski, -350, -250, 700, turtles.get(), 6))  # must be a thread as it calls triangle()
    thread.daemon = True  # thread dies when main thread (only non-daemon thread) exits.
    thread.start()
    
    process_queue()
    
    screen.exitonclick()
    

    When this runs, you should see the black outer triangle created followed by independent threads working on the three color triangles:

    enter image description here