ragardner
ragardner

Reputation: 1975

How to stop GUI from freezing when running a loop / function?

I am trying to add threading to a Python 3.63 Tkinter program where a function will run but the GUI will still be responsive, including if the user wants to close the program while the function is running.

In the example below I have tried to run a simple printing to console function on a separate thread to the GUI mainloop so the user could click the X in the top right to close the program while the loop is running if they so wish.

The error I am getting is:

TypeError: start() takes 1 positional argument but 2 were given

try:
    import tkinter as tk
    import queue as queue
except:
    import Tkinter as tk
    import Queue as queue
import threading

def center(toplevel,desired_width=None,desired_height=None):
    toplevel.update_idletasks()
    w, h = toplevel.winfo_screenwidth() - 20, toplevel.winfo_screenheight() - 100
    if desired_width and desired_height:
        size = (desired_width,desired_height)
    else:
        size = tuple(int(Q) for Q in toplevel.geometry().split("+")[0].split("x"))
    toplevel.geometry("%dx%d+%d+%d" % (size + (w/2 - size[0]/2, h/2 - size[1]/2)))

class ThreadedTask(threading.Thread):
    def __init__(self,queue):
        threading.Thread.__init__(self)
        self.queue = queue
    def run(self,func):
        func()

class app(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        center(self,desired_width=500,desired_height=400)
        self.queue = queue.Queue()
        self.run_func_button = tk.Button(self,
                                         text="Run Function",
                                         font=("Calibri",20,"bold"),
                                         command=self.run_func)
        self.run_func_button.pack()

    def run_func(self):
        ThreadedTask(self.queue).start(self.count_to_1500)

    def count_to_1500(self):
        for i in range(1500):
            print (i)

app_start = app()
app_start.mainloop()

Upvotes: 1

Views: 1854

Answers (2)

user48956
user48956

Reputation: 15810

Thread.start takes no parameters: https://docs.python.org/3/library/threading.html

The correct way to use a Thread is:

# Will call func(*args, **kwargs)
t = threading.Thread(target=func, args=(), kwargs={})
t.start()

t.join()

The join is important. Without it you will have many zombie threads in your app, which will also prevent your app from shutting down cleanly.

Another pattern is to use a daemon thread, which processes a queue. daemon threads are automatically killed when the program exits.

def worker(q):
    while True:
      try:
        f = q.get()
        q.task_done()
        if f is None: return
        f()
      except Exception:
        import traceback
        traceback.print_exc()

q = Queue.Queue()    
t = threading.Thread(target=worker, args=(q,))
t.daemon=True
t.start()


# f is a no-arg function to be executed
q.put(f) 


# Call at shutdown
q.join()

To run several tasks at the same time, start many threads.

Yet another method, use multiprocessing.pool.ThreadPool

from multiprocessing.pool import ThreadPool

# Create at startup
pool = ThreadPool(8)

# For each treaded task
pool.apply_async(func, args, kwds) 

# Call at shutdown
pool.close()
pool.join()

... which works, more or less, as the above.

I recommend reading:

https://docs.python.org/2/library/multiprocessing.html#multiprocessing-programming

Upvotes: 1

furas
furas

Reputation: 143002

See doc threading - start() doesn't use arguments but you use .start(self.count_to_1500) - and this gives your error.

You could use

Thread(target=self.count_to_1500).start()

or

Thread(target=self.count_to_1500, args=(self.queue,)).start()

if you define

def count_to_1500(self, queue):

EDIT: working example with thread which put in quoue and method which get data from queue.

try:
    import tkinter as tk
    import queue as queue
except:
    import Tkinter as tk
    import Queue as queue
import threading
import time

def center(toplevel,desired_width=None,desired_height=None):
    toplevel.update_idletasks()
    w, h = toplevel.winfo_screenwidth() - 20, toplevel.winfo_screenheight() - 100
    if desired_width and desired_height:
        size = (desired_width,desired_height)
    else:
        size = tuple(int(Q) for Q in toplevel.geometry().split("+")[0].split("x"))
    toplevel.geometry("%dx%d+%d+%d" % (size + (w/2 - size[0]/2, h/2 - size[1]/2)))

class app(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        center(self,desired_width=500,desired_height=400)
        self.queue = queue.Queue()
        self.run_func_button = tk.Button(self,
                                         text="Run Function",
                                         font=("Calibri",20,"bold"),
                                         command=self.run_func)
        self.run_func_button.pack()

    def run_func(self):
        threading.Thread(target=self.count_to_1500).start()
        threading.Thread(target=self.count_to_1500_with_queue, args=(self.queue,)).start()
        self.check_queue()

    def count_to_1500(self):
        for i in range(10):
            print('1:', i)
            time.sleep(0.2)

    def count_to_1500_with_queue(self, queue):
        for i in range(10):
            print('put:', i)
            queue.put(i)
            time.sleep(1)
        queue.put('last')

    def check_queue(self):
        print("check queue")
        data = None
        if not self.queue.empty():
            data = self.queue.get()
            print('get:', data)
        if data != 'last':
            self.after(200, self.check_queue)

app_start = app()
app_start.mainloop()

Upvotes: 2

Related Questions