Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Thread Termination
#1
Hello,

This is my first multithreading Tkinter GUI and I have errors when closing.

The GUI is running 4 thread routines, and they seem to be running ok. However, when I select the shutdown button that calls the terminate_threads() fucntion, the Tkinter GUI closes but I always get errors. Can you confirm if I am terminating the threads correctly?

Also please advise if the starting and handling of the threads are correct?

Multi thread code running at the bottom of the Tkinter mainloop:

def do_every (interval, worker_func, iterations = 0): 
  
   if iterations != 1:
    threading.Timer (
      interval,
      do_every, [interval, worker_func, 0 if iterations == 0 else iterations-1]
    ).start ()

  worker_func ()


# call main_control every 10 seconds
do_every (10, main_control)  

# call write_to_csv every 5 seconds
do_every (5, write_to_csv)  

# call duration_timer every 1 seconds
do_every (1, duration_timer)

# call one_second_timer every 1 seconds
do_every (1, one_second_timer)  
  
root.mainloop()
Terminate threads on closing of program:

def terminate_threads():

   # terminate all threads
   root.destroy()
   sys.exit()
   root.quit()
Many thanks,

Tuurbo46
Reply
#2
You don’t stop any of the threads created by threading timer. You need to keep a reference to the value returned by threading.Timer() and use it to call Timer.cancel(). This is tricky with how you used recursion to implement a recurring thread. Each time you run your periodic function you destroy the old Timer and create a new one.

This code wraps the Thread maintenance in a class.
import tkinter as tk
import threading
import random


class PeriodicTask:
    """Call a function periodically."""
    def __init__(self, period, task, *args, **kwargs):
        self.period = period
        self.task = task
        self.args = args
        self.kwargs = kwargs
        self.timer = None

    def stop(self):
        """Stop running the task."""
        if self.timer:
            self.timer.cancel()
        self.timer = None

    def start(self):
        """Start running the task."""
        if self.timer is None:
            self.timer = threading.Timer(self.period, self.doit)
            self.timer.start()

    def doit(self):
        self.task(*self.args, **self.kwargs)
        self.timer = None
        self.start()


class Demo(tk.Tk):
    """Demonstrate using PeriodicTask."""
    def __init__(self):
        super().__init__()
        self.text = tk.StringVar(self, "")
        tk.Label(
            self, textvariable=self.text, width=30
        ).pack(side=tk.TOP, padx=10, pady=10)
        self.button = tk.Button(self, text="Run", command=self.toggle)
        self.button.pack(side=tk.TOP, padx=10, pady=(0, 10))
        self.task = None

    def destroy(self):
        if self.task:
            """Stop active task before destroying window."""
            self.task.stop()
        super().destroy()
        
    def toggle(self):
        """Start/stop the periodic task."""
        if self.task:
            self.button["text"] = "Run"
            self.task.stop()
            self.task = None
        else:
            self.button["text"] = "Stop"
            self.task = PeriodicTask(1, self.update_label, "This is a test".split())
            self.task.start()

    def update_label(self, words):
        self.text.set(random.choice(words))


Demo().mainloop()
You should look at threading.Event. I think its a better choice for what you are doing.
Reply
#3
Hello deanhystad,

Thank for your reply. I struggle with OOP, and your example is quite complex - for a none OOP person :-)

I keep dodging OPP, but its looking like I will have to learn it soon.

Could you edit a cut down version that calls a hello_world function once a second. And hopefully I should be able to work it out from there.

So:

* Start: hello_world thread

* Call: hello_world thread

* Terminate: hello_world thread

Cheers,

Tuurbo46
Reply
#4
I really think you should take a look at threading.Event. Timer is not a good choice for a recurring function call because the Timer is only good for one call and then you need to make a new Timer. Keeping track of the last timer is a challenge unless you use object oriented programming. Global variables and functions just don't work the same way. With threading.Event you create one Event (timer like thing) and reuse it. Much easier to keep track of the event object.

This is my previous example without defining classes for the periodic task or tkinter window. Arrays and functions do their best to duplicate what the PeriodicTask class does.
import tkinter as tk
import threading
import random


# This is the PeriodicTask class converted to an array to hold all
# class attributes and methods written as functions.


def periodic_task(period, func, *args, **kwargs):
    """Creates a periodic task datastructure.

    Periodic task attributes:
    [0] Timer object.
    [1] Period in seconds.
    [2] Function to call.
    [3] Positional arguments to pass to function.
    [4] Keyword arguments to pass to function.
    """
    return [None, period, func, args, kwargs]


def task_stop(task):
    """Stop running the task."""
    if task[0]:
        task[0].cancel()
    task[0] = None


def task_start(task):
    """Start running the task."""
    if task[0] is None:
        task[0] = threading.Timer(task[1], task_execute, [task])
        task[0].start()


def task_execute(task):
    task[2](*task[3], **task[4])
    task[0] = None
    task_start(task)


# This is the main window from my previous example written without
# defining a class.

def on_closing():
    """Called when user closes the window."""
    task_stop(hello_task)
    root.destroy()


def toggle():
    """Called when user clicks the button."""
    if button["text"] == "Stop":
        button["text"] = "Run"
        task_stop(hello_task)
    else:
        button["text"] = "Stop"
        task_start(hello_task)


def update_label(words):
    """Called periodically to change the label text."""
    label["text"] = random.choice(words)


hello_task = periodic_task(1, update_label, "This is a test".split())
root = tk.Tk()
root.protocol("WM_DELETE_WINDOW", on_closing)
label = tk.Label(root, text="", width=30)
label.pack(side=tk.TOP, padx=10, pady=10)
button = tk.Button(root, text="Run", command=toggle)
button.pack(side=tk.BOTTOM, padx=10, pady=(0, 10))
root.mainloop()
Hopefully you can compare the two and see there is very little mystery in classes. The class __init__() method initializes attributes for the class instance, much like the PeriodicTask function does using an array. The methods defined for the PeriodicTask class become function calls in the no-classes example. No radical design changes, just organizing the code in a sligthly different way. The main difference is the classes do a better job showing the relationship between data and functions, and the syntax is easier to understand and prettier.

Here's a simple example that uses the class, but without any tkinter stuff.
import threading
import time
 
 
class PeriodicTask:
    """Call a function periodically."""
    def __init__(self, period, task, *args, **kwargs):
        self.period = period  # Wait before calling function
        self.task = task  # The function to execute periodically
        self.args = args  # positional arguments for the function
        self.kwargs = kwargs  # Keyword arguments for the function
        self.timer = None  # The active threading.Timer object.
 
    def stop(self):
        """Stop running the task."""
        if self.timer:
            self.timer.cancel()
        self.timer = None
 
    def start(self):
        """Start running the task."""
        if self.timer is None:
            self.timer = threading.Timer(self.period, self.doit)
            self.timer.start()
 
    def doit(self):
        """Execute the periodic function."""
        self.task(*self.args, **self.kwargs)
        self.timer = None  # Schedule to call function atain
        self.start()
 
 
hello_world = PeriodicTask(5, print, "Hello World!")
hello_world.start()
time.sleep(20)
hello_world.stop()
Reply
#5
deanhystad

I have taken your example and I am trying to build it into calling all of my existing functions. However it crashes after the first function call 'main_control'. It will work on one function call, but if I run more than one it crashes? I eventually want to have a 'start_button()' function and add all the 'starts' and a 'stop_button()' function to add all the 'stops', but it doesnt want to work like this.

Cheers,
Tuurbo46

import threading
import time
 

class PeriodicTask:
    """Call a function periodically."""
    def __init__(self, period, task, *args, **kwargs):
        self.period = period  # Wait before calling function
        self.task = task  # The function to execute periodically
        self.args = args  # positional arguments for the function
        self.kwargs = kwargs  # Keyword arguments for the function
        self.timer = None  # The active threading.Timer object.
  
    def stop(self):
        """Stop running the task."""
        if self.timer:
            self.timer.cancel()
        self.timer = None
  
    def start(self):
        """Start running the task."""
        if self.timer is None:
            self.timer = threading.Timer(self.period, self.doit)
            self.timer.start()
  
    def doit(self):
        """Execute the periodic function."""
        self.task(*self.args, **self.kwargs)
        self.timer = None  # Schedule to call function atain
        self.start()



# call main_control every 10 seconds 
def main_control(): 
    print('Main Control')
    
# call write to csv every 5 seconds 
def write_to_csv(): 
    print('Write to CSV')
    
# call duration timer every 1 seconds 
def duration_timer(): 
    print('Duration Timer')

# call one second timer every 1 seconds 
def one_second_timer(): 
    print('One Second Timer')



main_control = PeriodicTask(10, main_control())
main_control.start()
time.sleep(20)
main_control.stop()

write_to_csv = PeriodicTask(5, write_to_csv())
write_to_csv.start()
time.sleep(20)
write_to_csv.stop()
Reply
#6
A couple problems here:
def main_control(): 
    print('Main Control')

main_control = PeriodicTask(10, main_control())
The fist problem is you use the name "main_control" for two different things. You have a function named main_control and a variable named main_control. They need to have different names. When you do this:
main_control = PeriodicTask(10, main_control())
It assignes the PeriodicTask object as the value of the main_control variable. Now there is nothing that references the function and no way for you to call it. Reusing names is common in python, but you rarely want to reuse the name of a function.

The second problem is when you create the PeriodicTask. You are supposed to pass the function you want to call periodically. Your code calls the function and passes the return value (None) as the argument to PeriodicTask().

I would write the example like this:
main_task = PeriodicTask(10, main_control)  # Notice main_control, not main_control().
csv_task = PeriodicTask(5, write_to_csv)  # Notice different names for functions and variables.
main_task.start()
csv_task.start()
time.sleep(20)  # This sleep simulates the program going off and doing other work while the periodic tasks run periodically.
main_task.stop()  # When shutting down, stop all the tasks.
csv_task.stop()
You need to keep references to the PeriodicTask objects so you can shut them down at the end of the program. If that's the only time you refer to the PeriodicTasks, use a list to collect the references.
# Make a list of tasks.
 tasks = [
    PeriodicTask(10, main_control),
    PeriodicTask(5, write_to_csv)
]

for task in tasks:  # Use loop to start all tasks.
    task.start()

time.sleep(20)  # Let the tasks run for a while

for task in tasks:  # Use loop to stop all tasks.
    task.stop()
Reply
#7
One more thing. Using classis is not OOP. OOP is organizing code such that programs are mostly defined by the interaction of objects, and objects are a combination of data and procedures to manipulate that data. Classes are not OOP. Classes are a useful tool that can be used for OOP, but you can write OOP code in assembly language, and you can write code that has lots of classes but is not OOP.

It is usually easier to write Python code using classes. Code written using only functions can easily become a heaping pile of spaghetti. It can be difficult to follow what function calls what using what data. It can be almost impossible to predict how a change in one part of the code will affect other parts. This is much less a problem when you use classes. Classes encapsulate functions and data together in a well-defined interface. The interface isolates the code inside the class from changes outside the class.
Reply
#8
When doing multithreading in Tkinter there is important to understand after()
The after() method in Tkinter is used to schedule a function or action after a specified time,without freezing the user interface.
It acts as a non-blocking timer, ideal for GUI updates, animations, or periodic tasks.

Here is a clearer class-based Tkinter example using after() that:
  • updates a timer label every second
  • writes one row to a .csv file every 5 seconds
  • shuts down cleanly
import tkinter as tk
import csv
from datetime import datetime

class App:
    def __init__(self, root):
        self.root = root
        self.root.title("Tkinter after() CSV Example")
        self.root.geometry("300x150")
        self.running = True
        self.job_ids = {}
        self.counter = 0
        self.csv_file = "log.csv"

        self.label = tk.Label(root, text="Seconds: 0", font=("Arial", 14))
        self.label.pack(pady=20)
        self.button = tk.Button(root, text="Shutdown", command=self.on_close)
        self.button.pack(pady=10)
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)
        self.create_csv_file()
        self.start_job("timer", 1000, self.update_timer)
        self.start_job("csv_writer", 5000, self.write_to_csv)

    def create_csv_file(self):
        with open(self.csv_file, "w", newline="") as file:
            writer = csv.writer(file)
            writer.writerow(["timestamp", "counter"])

    def start_job(self, name, interval_ms, func):
        def wrapper():
            if not self.running:
                return
            func()
            if self.running:
                self.job_ids[name] = self.root.after(interval_ms, wrapper)
        self.job_ids[name] = self.root.after(interval_ms, wrapper)

    def update_timer(self):
        self.counter += 1
        self.label.config(text=f"Seconds: {self.counter}")
        print(f"Timer updated: {self.counter}")

    def write_to_csv(self):
        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        with open(self.csv_file, "a", newline="") as file:
            writer = csv.writer(file)
            writer.writerow([now, self.counter])
        print(f"Written to CSV: {now}, {self.counter}")

    def on_close(self):
        self.running = False
        for job_id in self.job_ids.values():
            try:
                self.root.after_cancel(job_id)
            except tk.TclError:
                pass
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    root.mainloop()
noisefloor likes this post
Reply
#9
I'd like to emphasize what @snippsat already said: don't reinvent the wheel! tkinter knows the after method for periodically calling a function. This is all controlled by the mainloop of tkinter, so no conflicts here.

Generally speaking, in GUI programming, the mainloop _must_ have the control, otherwise there a risk of unexpected behavior up to worst case freezing the whole GUI. In case there is a real need for threads, use the GUI's framework methods for creating threads. I believe tkinter doesn't have those methods for threads but the bigger frameworks like GTK and Qt have.

On

Quote: I struggle with OOP,

Do you really struggle with OOP or classes? As mentioned, it's quiet a big difference. Generally speaking, neither GUI programming nor concurrency is easy. Within Python, everything outside super simple GUIs should have the GUI inside a class, otherwise it's getting messy soon. Concurrency is not easy, as you have to thing outside a linear program flow. The same applies basically to GUIs, as the mainloop is basically some type of concurrency, reacting to whatever the user did whenever within the GUI. Adding you own concurrency on top of the GUI's concurrency makes it even harder.
Which of course doesn't mean that you need to stay away from it, but be ready for a steep learning curve.

Regards, noisefloor
Reply
#10
Thanks for all of your comments. I have added deanhystad's code into a simple tkinter example with a 'start' and 'stop' button. I want the threads to start when i select the 'start' button and the 'stop' to kill the threads and close the app.

So essentially my mainloop() is an extension of this style, it circulates, waiting for button presses and in parallel does timed events - quite important, reading and writing to hardware and saving data to csv.

Is the below structure sufficient and reliable, for an extended duration test? Also how to I check if my global variables etc are eating away at memory?

Cheers,
Tuurbo46

from tkinter import *        
from tkinter.ttk import *
import threading
import time
 

root = Tk() 
            
root.geometry('100x100')     

class PeriodicTask:
    # Call a function periodically.
    def __init__(self, period, task, *args, **kwargs):
        self.period = period  # Wait before calling function
        self.task = task  # The function to execute periodically
        self.args = args  # positional arguments for the function
        self.kwargs = kwargs  # Keyword arguments for the function
        self.timer = None  # The active threading.Timer object.
  
    def stop(self):
        # Stop running the task.
        if self.timer:
            self.timer.cancel()
        self.timer = None
  
    def start(self):
        # Start running the task.
        if self.timer is None:
            self.timer = threading.Timer(self.period, self.doit)
            self.timer.start()
  
    def doit(self):
        # Execute the periodic function.
        self.task(*self.args, **self.kwargs)
        self.timer = None  # Schedule to call function atain
        self.start()

# call main_control every 10 seconds 
def main_control(): 
    print('Main Control')
    
# call write to csv every 5 seconds 
def write_to_csv(): 
    print('Write to CSV')
    
# call duration timer every 1 seconds 
def duration_timer(): 
    print('Duration Timer')

# call one second timer every 1 seconds 
def one_second_timer(): 
    print('One Second Timer')

# start button
def start_button():
    main_task.start()
    csv_task.start()
    duration_task.start()
    one_second_task.start()
    
btn1 = Button(root, text = 'Start', command = start_button) 
btn1.pack(side = 'top') 

# stop button
def stop_button():
    main_task.stop()
    csv_task.stop() 
    duration_task.stop()
    one_second_task.stop()

    root.destroy()

btn2 = Button(root, text = 'Stop', command = stop_button) 
btn2.pack(side = 'bottom')     

main_task = PeriodicTask(10, main_control)
csv_task = PeriodicTask(5, write_to_csv)
duration_task = PeriodicTask(1, duration_timer)
one_second_task = PeriodicTask(1, one_second_timer)

root.mainloop() 
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Unexpected termination of program when using JDBC drivers in python with jaydebeapi skarface19 2 3,407 Feb-17-2024, 12:01 PM
Last Post: skarface19
  Error SQLite objects created in a thread can only be used in that same thread. binhduonggttn 3 22,029 Jan-31-2020, 11:08 AM
Last Post: DeaD_EyE
  random behavriour when handle process termination signal in Python hamzeah 2 3,394 Jul-31-2019, 07:32 AM
Last Post: hamzeah
  lambda-Amazon EC2:Remove instance termination protection if enabled and terminate ins dragan979 0 4,415 Jun-13-2018, 09:48 AM
Last Post: dragan979

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020