I'm using threading to run a long task, but I ran into an issue. The thread just hung while trying to set an IntVar after I clicked the close button. It doesn't even error. I don't want to use a daemon thread because the function is a critical part of the program, which might have consequences if it stops midway through (it deals with a bunch of files).
Here's an oversimplified version of my program, meant to demonstrate my issue.
import tkinter as tk
import tkinter.ttk as ttk
import threading
class Window(tk.Tk):
def __init__(this, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
this.threads = []
this.var = tk.IntVar(value=0)
this.label = ttk.Label(textvariable=this.var)
this.button = ttk.Button(text='Start counter', command=this.startCounter)
this.label.pack()
this.button.pack()
this.stop = False
this.protocol("WM_DELETE_WINDOW", this.close)
def startCounter(this):
thread = threading.Thread(target=this.counter)
this.threads.append(thread)
thread.start()
def counter(this):
while True:
if this.stop:
print(f'self.stop = ')
break
this.var.set(this.var.get() 1)
def close(this):
print('Stopping threads')
this.stop = True
this.waitThreads()
print('Stopped threads')
this.destroy()
def waitThreads(this):
for thread in this.threads:
thread.join()
Window().mainloop()
My program is using an InVar for a progress bar, not a counter, this was just the best way I could demonstrate the issue.
I tried a bunch of different methods to stop all threads, but none of them worked (that was before I knew what the issue was). For some reason in my actual program, if I log the var and the value of the var before the stop check, it's able to stop. I could not reproduce that with my test script.
I'm expecting the set var line to move on, or error instead of just hang.
Why is it hanging, and how can I fix this? I want to be able to safely stop the thread(s), and I don't want to use a daemon.
CodePudding user response:
you have a race condition, a deadlock, and an undefined behavior in your application ... that's how simple it is to mess up a small code snippet when multithreading.
the tk interpreter isn't threadsafe, and shouldn't be called from different threads, only the event_generate
function is threadsafe, incrementing the variable from another thread is likely going to crash the interpreter, it's also a race condition, and the results will be wrong, increments should only happen in the main thread, by generating an event from the other thread for the main thread to do the increment.
lastly you need to make your threads drop the GIL momentarily, this can be done by a small sleep time.sleep(0.000001)
.
import tkinter as tk
import tkinter.ttk as ttk
import threading
import time
class Window(tk.Tk):
def __init__(this, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
this.threads = []
this.var = tk.IntVar(value=0)
this.label = ttk.Label(textvariable=this.var)
this.button = ttk.Button(text='Start counter', command=this.startCounter)
this.bind("<<increment_counter>>",this.increment_var)
this.label.pack()
this.button.pack()
this.stop = False
this.protocol("WM_DELETE_WINDOW", this.close)
def startCounter(this):
thread = threading.Thread(target=this.counter)
this.threads.append(thread)
thread.start()
def increment_var(this, event):
this.var.set(this.var.get() 1)
def counter(this):
while True:
time.sleep(0.000001) # drop the GIL momentarily
if this.stop:
print(f'self.stop = ')
break
this.event_generate("<<increment_counter>>") # all increments happen in main thread
def close(this):
print('Stopping threads')
this.stop = True
this.waitThreads()
print('Stopped threads')
this.destroy()
def waitThreads(this):
for thread in this.threads:
thread.join()
Window().mainloop()
lastly, the first argument of a method is by convention called self
in python, calling it this
will confuse a lot of linters, parsers, other coders, autocorrect IDEs and documentation generators. please don't do that to everyone and use self
instead of this
.
CodePudding user response:
Comment out in line 38 #this.waitThreads()
Result when closing window:
Stopping threads
Stopped threads
self.stop =
In line 47, there is another way to improve. Just comment out line 38 and this too.
if __name__ == "__main__":
app = Window()
app.mainloop()