Question: I'm writing an app that generates a live histogram to be displayed in a Tkinter window. This is more or less how the app works:  A Histogram class is responsible for generating the embedded histogram inside the Tk window, collecting data and update the histogram accordingly. There is a 'Start' button that creates a thread which is responsible for  collecting data points and putting them in a queue, calling an update_histogram function which pulls the new data from the queue and redraws the histogram.   Since the function in the thread runs a loop indefinitely, there's also a 'Stop' button which stops the loop by setting an Event(). The stop function called by the button is also called when trying to close the window while the thread is running.  The issue Even if the same stop function is called by clicking the button or upon closing the window, if I try to close the window during a run the app freezes (is_alive() returns True), but not if I first click on the 'Stop' button and then close the window (is_alive() returns False). What am I doing wrong? MWE import tkinter as tk from tkinter import ttk from threading import Thread, Event from queue import Queue import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np class Histogram(): def __init__(self, root: tk.Tk): self.root = root self.buffer: list[float] = [] self.event = Event() self.queue = Queue() self.stopped = False self.fig, self.ax = plt.subplots(figsize=(4, 3), dpi=64, layout='tight') self.ax.set_xlim(0, 80) self.ax.set_ylim(0, 30) self.ax.set_xlabel('Time (ns)') self.ax.set_ylabel('Counts') self.canvas = FigureCanvasTkAgg(self.fig, master=self.root) self.canvas.draw() self.canvas.get_tk_widget().grid( column=0, columnspan=2, row=1, padx=6, pady=6, sticky='nesw' ) def start(self) -> None: self.cleanup() # Scrape canvas & buffer if restarting self.thread = Thread(target=self.follow) self.thread.start() self.stopped = True self.root.protocol('WM_DELETE_WINDOW', self.kill) def follow(self) -> None: count = 1 while not self.event.is_set(): data = np.random.normal(loc=40.0, scale=10.0) self.queue.put(data) self.update_histogram(n=count) count += 1 self.event.clear() self.stopped = True def update_histogram(self, n: int) -> None: data = self.queue.get() self.buffer.append(data) if n % 5 == 0: # Update every 5 new data points if self.ax.patches: _ = [b.remove() for b in self.ax.patches] counts, bins = np.histogram(self.buffer, bins=80, range=(0, 80)) self.ax.stairs(counts, bins, color='blueviolet', fill=True) # Add 10 to y upper limit if highest bar exceeds 95% of it y_upper_lim = self.ax.get_ylim()[1] if np.max(counts) > y_upper_lim * 0.95: self.ax.set_ylim(0, y_upper_lim + 10) self.canvas.draw() self.queue.task_done() def cleanup(self) -> None: if self.ax.patches: _ = [b.remove() for b in self.ax.patches] self.buffer = [] def stop(self) -> None: self.event.set() def kill(self) -> None: self.stop() all_clear = self.stopped while not all_clear: all_clear = self.stopped print(f'{self.thread.is_alive()=}') self.root.quit() self.root.destroy() def main(): padding = dict(padx=12, pady=12, ipadx=6, ipady=6) root = tk.Tk() root.title('Live Histogram') hist = Histogram(root=root) start_button = ttk.Button(root, text='START', command=hist.start) start_button.grid(column=0, row=0, **padding, sticky='new') stop_button = ttk.Button(root, text='STOP', command=hist.stop) stop_button.grid(column=1, row=0, **padding, sticky='new') root.mainloop() if __name__ == '__main__': main()  Note 1: The reason why I went for this fairly complicated setup is that I've learned that any other loop run in the main thread will cause the Tkinter mainloop to freeze, so that you can't interact with any widget while the loop is running. Note 2: I'm pretty sure I'm doing exactly what the accepted answer says in this post but here it doesn't work. This has been driving me crazy for days! Thank you in advance :)

Answer: The issue stems from a combination of thread synchronization problems and blocking behavior in the main (GUI) thread during window closure. The primary flaw is that your kill() method uses a non-thread-safe busy-wait loop (while not self.stopped) to monitor the background thread's state. This introduces three critical problems:  GUI Freeze: The busy-wait loop blocks the main thread, preventing Tkinter from processing its event queue, including window closure events and user interactions.  Thread Starvation Risk: Since the main thread repeatedly acquires the GIL without yielding, the background thread may be deprived of CPU time, delaying or preventing it from setting self.stopped = True.  Improper Synchronization: Using a simple boolean flag like self.stopped for thread communication is not thread-safe. While CPython’s GIL mitigates some risks, there’s no guarantee that the main thread will see the updated value in a timely or consistent manner, particularly in other Python implementations or complex scenarios.   Key fixes: 1. removed self.stopped = False 2. Use thread.join() with a timeout to ensure clean shutdown. def stop(self) -> None: self.event.set() if self.thread is not None: self.thread.join(timeout=0.1) self.thread = None  3. Initialize as None and track lifecycle. def __init__(self, ...): self.thread = None  4. Just call stop() and destroy window. def kill(self) -> None: self.stop() self.root.quit() self.root.destroy()  5. Clear event in start(). def start(self) -> None: self.event.clear() self.cleanup() self.thread = Thread(target=self.follow) self.thread.start()  Complete code after correction: import tkinter as tk from tkinter import ttk from threading import Thread, Event from queue import Queue import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np class Histogram(): def __init__(self, root: tk.Tk): self.root = root self.buffer: list[float] = [] self.event = Event() self.queue = Queue() self.thread = None # Initialize thread as None self.fig, self.ax = plt.subplots(figsize=(4, 3), dpi=64, layout='tight') self.ax.set_xlim(0, 80) self.ax.set_ylim(0, 30) self.ax.set_xlabel('Time (ns)') self.ax.set_ylabel('Counts') self.canvas = FigureCanvasTkAgg(self.fig, master=self.root) self.canvas.draw() self.canvas.get_tk_widget().grid( column=0, columnspan=2, row=1, padx=6, pady=6, sticky='nesw' ) def start(self) -> None: self.cleanup() # Scrape canvas & buffer if restarting self.event.clear() # Clear the event before starting self.thread = Thread(target=self.follow) self.thread.start() self.root.protocol('WM_DELETE_WINDOW', self.kill) def follow(self) -> None: count = 1 while not self.event.is_set(): data = np.random.normal(loc=40.0, scale=10.0) self.queue.put(data) self.update_histogram(n=count) count += 1 def update_histogram(self, n: int) -> None: data = self.queue.get() self.buffer.append(data) if n % 5 == 0: # Update every 5 new data points if self.ax.patches: _ = [b.remove() for b in self.ax.patches] counts, bins = np.histogram(self.buffer, bins=80, range=(0, 80)) self.ax.stairs(counts, bins, color='blueviolet', fill=True) # Add 10 to y upper limit if highest bar exceeds 95% of it y_upper_lim = self.ax.get_ylim()[1] if np.max(counts) > y_upper_lim * 0.95: self.ax.set_ylim(0, y_upper_lim + 10) self.canvas.draw() self.queue.task_done() def cleanup(self) -> None: if self.ax.patches: _ = [b.remove() for b in self.ax.patches] self.buffer = [] def stop(self) -> None: self.event.set() if self.thread is not None: self.thread.join(timeout=0.1) # Wait a short time for thread to finish self.thread = None def kill(self) -> None: self.stop() self.root.quit() self.root.destroy() def main(): padding = dict(padx=12, pady=12, ipadx=6, ipady=6) root = tk.Tk() root.title('Live Histogram') hist = Histogram(root=root) start_button = ttk.Button(root, text='START', command=hist.start) start_button.grid(column=0, row=0, **padding, sticky='new') stop_button = ttk.Button(root, text='STOP', command=hist.stop) stop_button.grid(column=1, row=0, **padding, sticky='new') root.mainloop() if __name__ == '__main__': main()  Output: