Multi-threading
IMPORTANT!
The first thing to understand with threading in PySimpleGUI (the tkinter port in particular) is that you must not make any calls into PySimpleGUI from a thread that are GUI related... with one exception: window.write_event_value
.
Multi-threading Versus Multi-processing (or subprocessing)
Multi-threading limits you to using only 1 of your CPU's cores. If you are CPU-bound and looking to achieve more performance, then you'll need to use Multi-processing.
It's more difficult to share information using multi-processing in Python however. Threads share the same memory, processes do not. See the section on the Exec APIs for more information about the multi-processing capabilities of PySimpleGUI.
Operations That Take a "Long Time"
You can't "starve" an event loop of CPU cycles. This is true for not just PySimpleGUI, but tkinter too and other GUI frameworks also share this property.
What's a long time? "A few seconds" is the time frame we're talking about here.
Your first indicator that something's not right is your window will change colors, going from this:
to this:
Eventually you may see this window that confirms you definitely have a problem to solve:
If you perform a lot of operations in your event loop and take "too long" between your call to window.read()
, then you may see this wonderful error message":
Don't Use sleep
Calls!
One of the easiest ways to get into trouble is to add calls to time.sleep
in your event loop. Don't do this. Instead you'll need to use the timeout
parameter in your call to window.read
and write some additional code to run a state machine or measure time passed using other Python calls like time.time
to determine if the right amount of time has passed.
If you must use sleeps, then you can call sleep from a thread instead of the event loop.
When a Sleep Makes Sense
One of the best examples of using sleeps would be in controlling hardware. If you wanted to blink an LED on a Raspberry Pi at a rate of 10 seconds, then you can simply turn on the LED, sleep 10 seconds, turn off the LED.
Maybe your code looks like this:
The problem with using this inside of an event loop is that while you're sleeping, the rest of the GUI is not running. This includes collecting mouse clicks, looking to see if the window has been closed with the X, etc. The GUI framework is not happy when you don't let it run frequently and that color change showed earlier is one indicator it's not happy.
Threads - The Solution
Don't freak out if you're a beginner. You don't need to learn the threading module to use threads in PySimpleGUI. We've simplified the use of threads so that you don't need to even import the threading module.
Can you can use the threading module if you want to use it rather than the more simplified PySimpleGUI interface.
Regardless of how you start your thread, you have the same restriction, you must not make any calls to PySimpleGUI from your thread, except for write_event_value
.
write_event_value
This is the important call that makes multi-threading work in PySimpleGUI. In order for your thread to talk to your main thread (your normal code is the "main thread"), this function does the communication for you. Call it causes an event to be sent to your main thread so that when it calls window.read()
the event is returns just like a Button click or any other window event.
The function is a method of the Window
class because you are sending an event to a specific window.
This call will send cause window.read()
to get an event '-KEY-'
when it calls window.read()
. In the values dictionary, the entry that matches the key will have the value 1234
. That is, values['-KEY-']
will equal 1234
:
Window.start_thread
and Window.perform_long_operation
These 2 calls are identical. They're aliases of each other. If seeing the word "thread" freaks you out, the maybe perform_long_operation
will calm your nerves.
To start a thread using these calls, you will provide 2 parameters:
- A lambda expression - don't freak out!
- A key signaling the thread ended
You will be placing the code to your thread in a function. This function will be executed as a thread. One mistake many people make with threads and passing functions around is that instead of passing the function, they call the function. Using a lambda keyword will stop this mix-up from happening and simplify the code.... honest... lambda simplifies your code in this situation.
Specifying The Function
The first parameter to Window.start_thread
or Window.perform_long_operation
is the name of your function, but instead of putting the name, we're going to write it as if you're calling the function and add the keyword lambda:
in front of it.
First let's write the function that will blink our LED. Instead of using a real LED, we're going to use our window. In our thread, we'll use write_event_value
to tell the GUI to turn the LED on and off.
If written as a function, our previous blink code may look like this:
Rather than using a real LED, we're going to simulate it in the window. To do this, we'll send a message to our GUI to "turn on" and "turn off" the "LED". Sending a message is done via our call to write_event_value
. We need the window we'll be communicating with. That's why you see it as a parameter to the function.
def blink_thread(window):
while True:
time.sleep(5) # sleep 5 second
window.write_event_value(('-THREAD-', 'LED ON'), 'LED ON')
time.sleep(5) # sleep 5 seconds
window.write_event_value(('-THREAD-', 'LED OFF'), 'LED OFF')
The lambda expression is simply what appears to be an ordinary call to your function with lambda:
in front of it. Seriously, that's all you need to know. This is the line of code that starts our thread:
Tuples as Keys For Threads
In this example, we're using a design pattern that's proven to be quite useful for these threads based applications. Instead of a simple string as a key, we're using a tuple. The reason is that the first item in the tuple is an easy indicator that the key is coming from the thread.
Rather than the typical string such as -THEAD-
, we're using this tuple ('-THREAD-', 1234)
. If you get an event with this key:
then event[0]
will have the value -THREAD-
. This allows us to mix in other string compares into the event loop without ever having to test if the event is a tuple explicitly. It's a clever trick.
So, we're using these tuples with the first item indicating it's from the thread and the second item being some additional information. What might that info be? Well, it'll be "Turn on the LED" or "Turn off the LED"
A Full Example
Here's the complete code for the example we've been stepping through above.
import PySimpleGUI as sg
import time
def blink_thread(window):
while True:
time.sleep(5) # sleep 5 second
window.write_event_value(('-THREAD-', 'LED ON'), 'LED ON')
time.sleep(5) # sleep 5 seconds
window.write_event_value(('-THREAD-', 'LED OFF'), 'LED OFF')
def main():
layout = [[sg.Text('Blinking LED Simulation')],
[sg.Text(sg.SYMBOL_CIRCLE_OUTLINE, text_color='red', key='-LED-', font='_ 25')],
[sg.Text(key='-MESSAGE-')],
[sg.Button('Start'), sg.Button('Exit')], ]
window = sg.Window('Blinking LED Window', layout)
# --------------------- EVENT LOOP ---------------------
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, 'Exit'):
break
window['-MESSAGE-'].update(f'{event}')
if event == 'Start':
window.start_thread(lambda: blink_thread(window), ('-THREAD-', '-THEAD ENDED-'))
elif event[0] == '-THREAD-':
if event [1] == 'LED ON':
window['-LED-'].update(sg.SYMBOL_CIRCLE)
elif event [1] == 'LED OFF':
window['-LED-'].update(sg.SYMBOL_CIRCLE_OUTLINE)
window.close()
if __name__ == '__main__':
main()
The dreaded "Tcl_AsyncDelete: async handler deleted by the wrong thread" error
This crash has plagued and mystified tkinter and PySimpleGUI users for some time now. It happens when the user is running multiple threads in their application. Even if the user doesn't make any calls that are into tkinter from a thread, this problem can still cause your program to crash.
I'm thrilled to say there's a solution and it's easy to implement. If you're getting this error, then here is what is causing it.
When you close a window and delete the layout, the tkinter widgets that were in use in the window are no longer needed. Python marks them to be handled by the "Garbage Collector". They're deleted but not quite gone from memory. Then, later, while your thread is running, the Python Garbage Collect algorithm decides it's time to run garbage collect. When it tells tkinter to free up the memory, the tkinter code looks to see what context it is running under. It sees that it's a thread, not the main thread, and generates this exception.
The way around this is actually quite easy.
When you are finished with a window, be sure to:
- Close the Window
- Set the
layout
variable to None - Set the
window
variable to None - Trigger Python's Garbage Collect to run immediately
The sequence looks like this in code:
import gc
# Do all your windows stuff... make a layout... show your window... then when time to exit
window.close()
layout = None
window = None
gc.collect()
This will ensure that the tkinter widgets are all deleted in the context of the main-thread and another thread won't accidentally run the Garbage Collect