Home > database >  Thread hangs when trying to write to tkinter IntVar when user closes the window
Thread hangs when trying to write to tkinter IntVar when user closes the window

Time:02-05

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()
  • Related