Tkinter: How to use threads to preventing main event loop from "freezing"
Asked Answered
C

5

79

I have a small GUI test with a "Start" button and a Progress bar. The desired behavior is:

  • Click Start
  • Progressbar oscillates for 5 seconds
  • Progressbar stops

The observed behavior is the "Start" button freezes for 5 seconds, then a Progressbar is displayed (no oscillation).

Here is my code so far:

class GUI:
    def __init__(self, master):
        self.master = master
        self.test_button = Button(self.master, command=self.tb_click)
        self.test_button.configure(
            text="Start", background="Grey",
            padx=50
            )
        self.test_button.pack(side=TOP)

    def progress(self):
        self.prog_bar = ttk.Progressbar(
            self.master, orient="horizontal",
            length=200, mode="indeterminate"
            )
        self.prog_bar.pack(side=TOP)

    def tb_click(self):
        self.progress()
        self.prog_bar.start()
        # Simulate long running process
        t = threading.Thread(target=time.sleep, args=(5,))
        t.start()
        t.join()
        self.prog_bar.stop()

root = Tk()
root.title("Test Button")
main_ui = GUI(root)
root.mainloop()

Based on the information from Bryan Oakley here, I understand that I need to use threads. I tried creating a thread, but I'm guessing that since the thread is started from within the main thread, it doesn't help.

I had the idea to place the logic portion in a different class, and instantiate the GUI from within that class, similar to the example code by A. Rodas here.

My question:

I can't figure out how to code it so that this command:

self.test_button = Button(self.master, command=self.tb_click)

calls a function that is located in the other class. Is this a Bad Thing to do or is it even possible? How would I create a 2nd class that can handle the self.tb_click? I tried following along to A. Rodas' example code which works beautifully. But I cannot figure out how to implement his solution in the case of a Button widget that triggers an action.

If I should instead handle the thread from within the single GUI class, how would one create a thread that doesn't interfere with the main thread?

Contributory answered 25/5, 2013 at 1:16 Comment(0)
D
84

When you join the new thread in the main thread, it will wait until the thread finishes, so the GUI will block even though you are using multithreading.

If you want to place the logic portion in a different class, you can subclass Thread directly, and then start a new object of this class when you press the button. The constructor of this subclass of Thread can receive a Queue object and then you will be able to communicate it with the GUI part. So my suggestion is:

  1. Create a Queue object in the main thread
  2. Create a new thread with access to that queue
  3. Check periodically the queue in the main thread

Then you have to solve the problem of what happens if the user clicks two times the same button (it will spawn a new thread with each click), but you can fix it by disabling the start button and enabling it again after you call self.prog_bar.stop().

import queue

class GUI:
    # ...

    def tb_click(self):
        self.progress()
        self.prog_bar.start()
        self.queue = queue.Queue()
        ThreadedTask(self.queue).start()
        self.master.after(100, self.process_queue)

    def process_queue(self):
        try:
            msg = self.queue.get_nowait()
            # Show result of the task if needed
            self.prog_bar.stop()
        except queue.Empty:
            self.master.after(100, self.process_queue)

class ThreadedTask(threading.Thread):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue
    def run(self):
        time.sleep(5)  # Simulate long running process
        self.queue.put("Task finished")
Dairying answered 25/5, 2013 at 8:17 Comment(15)
Another beautiful example. Thank you A. Rodas :) I have a follow up question: If I comment out self.master.after(100, self.process_queue) and replace it with simply self.process_queue() the behavior is the same. Is there a good reason to have the self.master.after... part?Contributory
Yes, with self.master.after(100, self.process_queue) you schedule this method each 100 milliseconds, while self.process_queue() constatly executes it without any delay between each call. There is no need to do that, so after is a better solution to check the content peridically.Dairying
Sorry, Rodas, I am in a similar situation as explained by the OP, but in my case I should call a function from another class when I press a button, but it continues to freeze. I am not so familiar with threading, so that's why I am asking how should I do it. Since I have to call a function (just when I click the button of the GUI) of an object created in the constructor of my GUI application, should I make my other class derive from Thread anyway?Khufu
Thank you so much for this. I've barely used threads and never queue, and your example helped me make a working gui with multiple com and debug windows.Gaskell
A. Rodas the potential problem with this I think is that if the user closes the GUI by clicking the X button then the threaded task will continue to runTridentine
@citizen2077 If you want to prevent users from doing that, you can handle the WM_DELETE_PROTOCOL and only destroying the GUI if the thread is not alive.Dairying
@A.Rodas Thanks for the response. Would this be only appropriate way to handle a user closing the GUI during a non-for loop threaded task? Is there no other way? perhaps a timeout parameter in the threaded task somewhere? I am thinking about asking a separate question with a minimal exampleTridentine
@citizen2077 Adding a handler would be the first step to define what happens if the root is closed using the window manager, but you can also use a flag to communicate the thread that it should stop its execution. Feel free to ask your question separately, since it is not strictly related with OP's question.Dairying
@A.Rodas just in case you're interested my question can be located here: #48611837Tridentine
@A.Rodas .after() actually runs the method only once. tcl.tk/man/tcl8.4/TclCmd/after.htmShawndashawnee
As explained at stupidpythonideas.blogspot.ru/2013/10/… , this approach is defective. First, it doesn't scale. Second, polling is inherently inferior to event handling.Shawndashawnee
@Shawndashawnee OP's question has little to do with scalability - it describes an scenario with a single background thread that runs for a few seconds. If you can provide any working example that shows a significant performance improvement, feel free to share it.Dairying
def run(self) under class ThreadedTask() is not accessed anywhere in your sample code. Where should it be placed to have a working simulation?Unlawful
It is internally executed on another thread by calling Thread.start(): docs.python.org/3/library/threading.html#threading.Thread.startDairying
Regarding your recent update: If you had previously imported via from Queue import Queue it would have only taken changing that one line to switch from Python 2 to Python 3. Also, it would have been possible to use super() in Python 2 and it would have still worked in Python 3 because the old syntax is still accepted.Showthrough
L
8

I will submit the basis for an alternate solution. It is not specific to a Tk progress bar per se, but it can certainly be implemented very easily for that.

Here are some classes that allow you to run other tasks in the background of Tk, update the Tk controls when desired, and not lock up the gui!

Here's class TkRepeatingTask and BackgroundTask:

import threading

class TkRepeatingTask():

    def __init__( self, tkRoot, taskFuncPointer, freqencyMillis ):
        self.__tk_   = tkRoot
        self.__func_ = taskFuncPointer        
        self.__freq_ = freqencyMillis
        self.__isRunning_ = False

    def isRunning( self ) : return self.__isRunning_ 

    def start( self ) : 
        self.__isRunning_ = True
        self.__onTimer()

    def stop( self ) : self.__isRunning_ = False

    def __onTimer( self ): 
        if self.__isRunning_ :
            self.__func_() 
            self.__tk_.after( self.__freq_, self.__onTimer )

class BackgroundTask():

    def __init__( self, taskFuncPointer ):
        self.__taskFuncPointer_ = taskFuncPointer
        self.__workerThread_ = None
        self.__isRunning_ = False

    def taskFuncPointer( self ) : return self.__taskFuncPointer_

    def isRunning( self ) : 
        return self.__isRunning_ and self.__workerThread_.isAlive()

    def start( self ): 
        if not self.__isRunning_ :
            self.__isRunning_ = True
            self.__workerThread_ = self.WorkerThread( self )
            self.__workerThread_.start()

    def stop( self ) : self.__isRunning_ = False

    class WorkerThread( threading.Thread ):
        def __init__( self, bgTask ):      
            threading.Thread.__init__( self )
            self.__bgTask_ = bgTask

        def run( self ):
            try :
                self.__bgTask_.taskFuncPointer()( self.__bgTask_.isRunning )
            except Exception as e: print repr(e)
            self.__bgTask_.stop()

Here's a Tk test which demos the use of these. Just append this to the bottom of the module with those classes in it if you want to see the demo in action:

def tkThreadingTest():

    from tkinter import Tk, Label, Button, StringVar
    from time import sleep

    class UnitTestGUI:

        def __init__( self, master ):
            self.master = master
            master.title( "Threading Test" )

            self.testButton = Button( 
                self.master, text="Blocking", command=self.myLongProcess )
            self.testButton.pack()

            self.threadedButton = Button( 
                self.master, text="Threaded", command=self.onThreadedClicked )
            self.threadedButton.pack()

            self.cancelButton = Button( 
                self.master, text="Stop", command=self.onStopClicked )
            self.cancelButton.pack()

            self.statusLabelVar = StringVar()
            self.statusLabel = Label( master, textvariable=self.statusLabelVar )
            self.statusLabel.pack()

            self.clickMeButton = Button( 
                self.master, text="Click Me", command=self.onClickMeClicked )
            self.clickMeButton.pack()

            self.clickCountLabelVar = StringVar()            
            self.clickCountLabel = Label( master,  textvariable=self.clickCountLabelVar )
            self.clickCountLabel.pack()

            self.threadedButton = Button( 
                self.master, text="Timer", command=self.onTimerClicked )
            self.threadedButton.pack()

            self.timerCountLabelVar = StringVar()            
            self.timerCountLabel = Label( master,  textvariable=self.timerCountLabelVar )
            self.timerCountLabel.pack()

            self.timerCounter_=0

            self.clickCounter_=0

            self.bgTask = BackgroundTask( self.myLongProcess )

            self.timer = TkRepeatingTask( self.master, self.onTimer, 1 )

        def close( self ) :
            print "close"
            try: self.bgTask.stop()
            except: pass
            try: self.timer.stop()
            except: pass            
            self.master.quit()

        def onThreadedClicked( self ):
            print "onThreadedClicked"
            try: self.bgTask.start()
            except: pass

        def onTimerClicked( self ) :
            print "onTimerClicked"
            self.timer.start()

        def onStopClicked( self ) :
            print "onStopClicked"
            try: self.bgTask.stop()
            except: pass
            try: self.timer.stop()
            except: pass                        

        def onClickMeClicked( self ):
            print "onClickMeClicked"
            self.clickCounter_+=1
            self.clickCountLabelVar.set( str(self.clickCounter_) )

        def onTimer( self ) :
            print "onTimer"
            self.timerCounter_+=1
            self.timerCountLabelVar.set( str(self.timerCounter_) )

        def myLongProcess( self, isRunningFunc=None ) :
            print "starting myLongProcess"
            for i in range( 1, 10 ):
                try:
                    if not isRunningFunc() :
                        self.onMyLongProcessUpdate( "Stopped!" )
                        return
                except : pass   
                self.onMyLongProcessUpdate( i )
                sleep( 1.5 ) # simulate doing work
            self.onMyLongProcessUpdate( "Done!" )                

        def onMyLongProcessUpdate( self, status ) :
            print "Process Update: %s" % (status,)
            self.statusLabelVar.set( str(status) )

    root = Tk()    
    gui = UnitTestGUI( root )
    root.protocol( "WM_DELETE_WINDOW", gui.close )
    root.mainloop()

if __name__ == "__main__": 
    tkThreadingTest()

Two import points I'll stress about BackgroundTask:

1) The function you run in the background task needs to take a function pointer it will both invoke and respect, which allows the task to be cancelled mid way through - if possible.

2) You need to make sure the background task is stopped when you exit your application. That thread will still run even if your gui is closed if you don't address that!

Lazos answered 29/1, 2017 at 22:29 Comment(16)
Wow, I don't think you understand how the after() method works. In the accepted answer, the self.master.after(100, self.process_queue) doesn't call self.process_queue recursively. It only schedules for it to be run again in 100 ms. The second argument is just the name of the function, not a call to it—and it only does this when exception Queue.Empty was raised, meaning that the ThreadedTask hasn't put anything in queue yet, so it need to keep checking.Showthrough
@Showthrough I hope you are right! I ran that with some slight tweaks, and it crashed due to having too many recursive calls. In other languages and libraries I've used very similar repeating timers without a problem. I would love to see that work the way it seems like it should (i.e. non recursively). I will play with that and retract my answer when I have success. Although my BackgroundTask class still works well at least in my example - I haven't tested it enough to know what it will choke on with tk being non thread safe, however, an that concerned me about it!Lazos
I'm really confident about what I said. Tkinter not being thread-safe doesn't mean you can't use it in a multi-threaded application. Only that you must limit the number of threads accessing Tkinter concurrently to one (and that is usually left up to the main thread). My answer to another Tkinter question has an example of that being done.Showthrough
You are quite correct! I retract my harsh comments. I've radically altered my post. I absolutely did see that recursion crash, but there must have been something else going on.Lazos
I will note that I've found some issues with using my BackgroundTask class. e.g. If you create a dialog box from that thread it will appear somewhere off the screen and cause you problems!Lazos
If you created a dialog box in one thread and also used Tkinter in any other thread (like the main one), then there will be problems because that's not adhering to the rule to only one thread accesses the GUI module. In other words. having any other tasks update the Tk controls is a no-no. The code in your updated answer is only accessing Tkinter from one background thread, so it might appear to work OK. It wouldn't if more than one instance were ever created, and it's very odd to be having the definition of class UnitTestGUI (or any class) initializing and running the GYI mainloop.Showthrough
Thanks martineau, that's very helpful. The way I've used this so far in practice has been to allow 1 long running processes at a time to run in the background without locking up the gui. When I fire off such a thing, I disable any controls that would fire off another one, and just allow the user to do quick and simple tasks in the foreground. When the worker thread is done, the rest of the controls are re-enabled. Perhaps my class should be defined to enforce this? It should be a singleton maybe with a virtual function to disable and re-enable controls that would use it?Lazos
If you look closely at the indents class UnitTestGUI doesn't run the mainloop. That's outside of it.Lazos
Oops. sorry, you're right, the mainloop() call is part of the tkThreadingTest() function, not the class definition—so that part of the code isn't as strange as I thought. I stand by the rest of my comment(s), however. Thanks for updating your post, BTW.Showthrough
You could make it so it wasn't necessary to stop the background task(s) before exiting the application by setting their daemon attribute to True. See my answer to a different question for more details and links to the relevant documentation.Showthrough
Hello, thank you for the explanation and if I need past parameters to BackgroundTaskGenni
You're welcome @virtualsets. Are you asking how you could pass parameters to a BackgroundTask?Lazos
@ BuvinJ hello, yes I am Asking , I can not rescribe to send parameters to functions like myLongProcess . reggardsGenni
@Genni Ignoring thread safety, and the possible need for mutexs, please start in this way: Add some public attributes to class BackgroundTask. Then change the line in WorkerThread self.__bgTask_.taskFuncPointer()( self.__bgTask_.isRunning ) to include the new parameters e.g. self.__bgTask_.taskFuncPointer()( self.__bgTask_.isRunning, self.__bgTask_.myCustomParm )Lazos
After that, you can change your BackgroundTask object's new public attribute and it will flow through these layers.Lazos
Did that help? Do I need to be more clear, detailed?Lazos
C
6

I have used RxPY which has some nice threading functions to solve this in a fairly clean manner. No queues, and I have provided a function that runs on the main thread after completion of the background thread. Here is a working example:

import rx
from rx.scheduler import ThreadPoolScheduler
import time
import tkinter as tk

class UI:
   def __init__(self):
      self.root = tk.Tk()
      self.pool_scheduler = ThreadPoolScheduler(1) # thread pool with 1 worker thread
      self.button = tk.Button(text="Do Task", command=self.do_task).pack()

   def do_task(self):
      rx.empty().subscribe(
         on_completed=self.long_running_task, 
         scheduler=self.pool_scheduler
      )

   def long_running_task(self):
      # your long running task here... eg:
      time.sleep(3)
      # if you want a callback on the main thread:
      self.root.after(5, self.on_task_complete)

   def on_task_complete(self):
       pass # runs on main thread

if __name__ == "__main__":
    ui = UI()
    ui.root.mainloop()

Another way to use this construct which might be cleaner (depending on preference):

tk.Button(text="Do Task", command=self.button_clicked).pack()

...

def button_clicked(self):

   def do_task(_):
      time.sleep(3) # runs on background thread
             
   def on_task_done():
      pass # runs on main thread

   rx.just(1).subscribe(
      on_next=do_task, 
      on_completed=lambda: self.root.after(5, on_task_done), 
      scheduler=self.pool_scheduler
   )
Canterbury answered 2/9, 2020 at 3:56 Comment(2)
How would I use this to update a label, then run long task?Cosignatory
@CaiAllin just update your label on the line above rx.just(1) or rx.empty()Canterbury
R
5

The problem is that t.join() blocks the click event, the main thread does not get back to the event loop to process repaints. See Why ttk Progressbar appears after process in Tkinter or TTK progress bar blocked when sending email

Ribera answered 25/5, 2013 at 1:32 Comment(0)
D
0

In my program, the MainThread would end before the other threads would start. For this, I added just this:

while True: #Keeps MainThread active forever
        time.sleep(1)

In VSCode (and other programs), you can enable debug and run, and check the 'call stack' area for the MainThread. If you don't see it on there near the place of error, that means its not active at that point. Try using the above code to combat that.

Drawbridge answered 10/5 at 3:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.