Strange behaviour of tkinter when using multiple threads - python

When a new thread destroys a tkinter Tk object whose mainloop is in the main thread, the new thread seems to get stuck and never continues or completes. Why does this happen? The code below shows an example of what I mean:
from tkinter import *
from threading import *
import time
win = Tk()
def exit_after_time():
time.sleep(2)
win.destroy()
thread = Thread(target=exit_after_time)
thread.start()
win.mainloop()
print("mainloop quit")
thread.join()
print("thread joined")
When this is run, the mainloop is quit after 2 seconds, but the thread is never joined. At the moment I'm just working around this by making the thread a daemon and not waiting for it to finish, but I'd like to know what's actually going on.

Related

How to properly close a tkinter window with multiple threads?

I'm writing a script that takes some time to complete, but want to give some feedback to the user showing that the program is actually running. I'm trying to use the progress bar from tkinter because it's simple, but when the window closes, the program continues running in the background.
import sys
from time import sleep
from threading import Thread
import tkinter as tk
import tkinter.ttk as ttk
def work(win):
sleep(10)
win.destroy()
root = tk.Tk()
p = ttk.Progressbar(root, mode="indeterminate")
p.pack()
t = Thread(target=lambda: work(root))
t.start() # start thread
p.start() # start progressbar
root.mainloop()
sys.exit()
Here's essentially what the code does. I've tried putting sys.exit() after the thread closes the window, but that yields the same result.
After some debugging it seems like the program keeps running the main loop of root even though I destroyed it, so how can I make sure the program terminates the script properly?

How does a thread close when using Tkinter

I know this question has been asked numerous times before, but they all use people's code and classes/various functions, etc, so its been a bit difficult to try and follow/understand what's going on. I wanted a more simpler setup to understand what's happening here and how it all works.
When you thread a function, once the function is complete, the thread is closed:
import threading
def fun():
x=0
while x<1000:
x+=1
threading.Thread(target=fun).start()
So I decided to take this idea one step further with Tkinter.
import tkinter as tk
from tkinter import *
from tkinter import messagebox as mb
import threading
def fun():
x=0
while x<10000900:
x+=1
if x == 50:
print(x)
def main():
while True:
fun()
if mb.askquestion('Replay', 'Would you like to play another round?') != 'yes':
root.destroy()
break
root = tk.Tk()
root.geometry('600x600')
threading.Thread(target=main).start()
root.mainloop()
The idea being, that when the main function was broken (based on the user response), the thread would also close automatically. Additionally, so would the Tkinter window. However, when the above script is run, the Tkinter window does close, but the terminal still indicates something, which I assume is the thread. What I don't understand is why in the first case where I use threading, the program ends properly, whereas the 2nd one doesn't.
When you execute root.destroy() you kill the main thread (mainloop) as well as the extra thread running your main funtion. That way the break statement never gets executed - the thread that would execute that statement is abruptly ended.
If you replace root.destroy() with root.after(10, root.destroy) the program acts as expected. This is because you're delaying the call to root.destroy() by some time (10 ms). This delay allows the break statement to be executed since the thread is still alive for 10 ms.

Can I run a while loop in the background at the same time as other things? [duplicate]

I am trying to run a While Loop in order to constantly do something. At the moment, all it does is crash my program.
Here is my code:
import tkinter
def a():
root = tkinter.Tk()
canvas = tkinter.Canvas(root, width=800, height=600)
while True:
print("test")
a()
It will loop the print statement, however the actual canvas refuses to open.
Are there any viable infinite loops that can work alongside Tkinter?
Extra Information
When I remove the While True statement, the canvas reappears again.
Tkinter hangs unless it can execute its own infinite loop, root.mainloop. Normally, you can't run your own infinite loop parallel to Tkinter's. There are some alternative strategies, however:
Use after
after is a Tkinter method which causes the target function to be run after a certain amount of time. You can cause a function to be called repeatedly by making itself invoke after on itself.
import tkinter
#this gets called every 10 ms
def periodically_called():
print("test")
root.after(10, periodically_called)
root = tkinter.Tk()
root.after(10, periodically_called)
root.mainloop()
There is also root.after_idle, which executes the target function as soon as the system has no more events to process. This may be preferable if you need to loop faster than once per millisecond.
Use threading
The threading module allows you to run two pieces of Python code in parallel. With this method, you can make any two infinite loops run at the same time.
import tkinter
import threading
def test_loop():
while True:
print("test")
thread = threading.Thread(target=test_loop)
#make test_loop terminate when the user exits the window
thread.daemon = True
thread.start()
root = tkinter.Tk()
root.mainloop()
But take caution: invoking Tkinter methods from any thread other than the main one may cause a crash or lead to unusual behavior.

pyside qapplication exec with while loop

Basically I have a program that will create a basic hello world program in PySide qt framework. The difference is that it does print("loop") in a while loop before exec_() is called. The problem with that the loop won't finish until the user is done with the program, so it will only call exec_() when the loop's done.
My problem is that if you run it like this, print("loop") will run, but the window won't respond, and doesn't display "Hello, loop!"). If you indent qt_app.exec_() under while running:, then the window will respond, but print("loop") only executes once before closing the window, and executes only once after closing it.
I need to be able to have the main window be responding while its printing "loop" to the console multiple times.
import sys
from PySide.QtCore import *
from PySide.QtGui import *
qt_app = QApplication(sys.argv)
label = QLabel('Hello, loop!')
label.show()
running = True #only set to False when user is done with app in the real code.
while running:
#I am handling connections here that MUST be in continual while loop
print("loop")
qt_app.exec_()
If you want to have a GUI application you have to let the GUI event loop take over the main thread.
The solution for your problem would be to create a separate thread that will do the printing while you let the qt event loop take over the main thread.
Your thread will be running in the background, doing it's stuff and (since I set it to be daemon) it will stop when the application finishes, or the running variable is set to False.
import sys
import time
import threading
from PySide.QtCore import *
from PySide.QtGui import *
qt_app = QApplication(sys.argv)
label = QLabel('Hello, loop!')
label.show()
running = True #only set to False when user is done with app in the real code.
def worker():
global running
while running:
#I am handling connections here that MUST be in continual while loop
print("loop")
time.sleep(0.5)
thread = threading.Thread(target=worker)
thread.setDaemon(True)
thread.start()
qt_app.exec_()
But this is a bad example since you shouldn't use global mutable variables in thread without locking, etc. etc. etc... But that's all in the docs.

Tkinter and multi-threading

I was using the following code to examine if Tkinter works along with multithreading. But the following code doesn't work (the Gui becomes unresponsive as soon as it runs). Can anyone please explain why it doesn't work?
from threading import Thread
import tkinter as tk
window = tk.Tk()
label = tk.Label(window, text='Hello')
label.pack()
def func():
i = 1
while True:
label['text'] = str(i)
i += 1
Thread(target=func).start()
Thread(target=window.mainloop).start()
It doesn't work because Tkinter doesn't support multithreading. Everything that interacts with a Tkinter widget needs to run in the main thread. If you want to use multithreading, put the GUI in the main thread and your other code in a worker thread, and communicate between them with a thread safe queue.

Categories

Resources