[Python] Gracefully terminate tkinter app (including multithreading, multiprocessing,...

Discussão em 'Python' iniciado por Stack, Outubro 27, 2024.

  1. Stack

    Stack Membro Participativo

    Question


    How to make a python tkinter app close gracefully at the end of normal operation and if user terminates the window interrupting the processing being performed?

    Details


    The tkinter app launches a long running function long.work() on click of a button, this function is started in a new thread which allows the tkinter app to be responsive during the long execution.

    Some of the functions called in work() are multithreaded via ThreadPoolExecutor (concurrent_tasks.do_multi_threads()), and others are multiprocessed via joblib Parallel() and delayed() (parallel_tasks.do_multi_process()).

    Therefore, graceful ending must handle cases where no user termination occurs, user termination occurs after function completes, user interruption occurs in the multithreadeded and multiprocess states, as well as handle closing of loggers that use QueueListener and QueueHandler to write log messages to a log file.

    I have looked at some similar questions here on SO, but none cover all of the use cases required.

    Example


    My approach so far gives error when closing window interrupting the multithreaded function (among others):

    app.py


    import logging
    from logging.handlers import QueueHandler, QueueListener
    import sys
    import multiprocessing
    import threading
    import tkinter as tk
    from tkinter import Tk, ttk
    from joblib.externals.loky import get_reusable_executor

    import long


    def finish(
    logger: logging.Logger,
    listener: QueueListener = None,
    log_queue: multiprocessing.Queue = None,
    threads: bool = False,
    lock: threading.Lock = None,
    root: tk.Tk = None,
    exit_code: int = 0,
    ):
    # QueueHandler
    if log_queue is not None:
    log_queue.put_nowait(None)

    # QueueListener
    if listener is not None:
    listener.stop()

    # joblib
    get_reusable_executor(kill_workers=True).shutdown(wait=True)

    # multiprocessing
    for p in multiprocessing.active_children():
    p.terminate()
    p.join()

    # threading
    if threads is True:
    for thread in threading.enumerate():
    if thread is not threading.current_thread():
    thread.join(timeout=1)

    # logger
    for handler in logger.handlers:
    handler.flush()
    handler.close()
    logger.handlers.clear()

    # lock
    if lock is not None and lock.locked():
    lock.release()

    # tkinter
    if root is not None:
    root.destroy()
    sys.exit(exit_code)


    def main():
    root = Tk()

    # logger
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)
    log_fmt = logging.Formatter(
    "%(asctime)s (%(levelname)s) %(message)s", datefmt="%H:%M:%S"
    )
    file_handler = logging.FileHandler("log_file.log")
    file_handler.setFormatter(log_fmt)
    file_handler.setLevel(logging.DEBUG)

    # note also a StreamHandler is added to QueueListener

    manager = multiprocessing.Manager()
    log_queue = manager.Queue()
    queue_handler = QueueHandler(log_queue)
    logger.addHandler(queue_handler)

    listener = QueueListener(log_queue, file_handler, respect_handler_level=True)
    listener.start()

    def begin():
    # entry_vals = get Entry values...
    exit_code = 0
    lock = threading.Lock()

    def terminate():
    finish(logger, listener, log_queue, threads=True, lock=lock, root=root, exit_code=exit_code)

    root.protocol("WM_DELETE_WINDOW", terminate)

    def start(): # EDIT
    try:
    long.work(entry_vals)
    finally:
    button.config(state=tk.NORMAL) # re-enable
    finish(logger, listener, log_queue, threads=True, lock=lock, root=None, exit_code=exit_code)

    try: # EDIT (indentation)
    set_progress = entry_vals.get("update_status")
    if set_progress: # EDIT
    set_progress("begin", 1) # EDIT
    logging. Debug("Starting GUI...")
    button.config(state=tk.DISABLED) # disable button
    threading.Thread(target=start).start() # EDIT
    except Exception as e:
    exit_code = 1
    logging.exception(e)
    finish(logger, listener, log_queue, threads=False, lock=None, root=None, exit_code=exit_code)

    frame = ttk.Frame(root)
    frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
    button = ttk.Button(frame, text="Start", command=begin)

    root.mainloop()

    if __name__ == "__main__":
    main()

    long.py


    import logging
    from concurrent. Futures import ThreadPoolExecutor
    from functools import partial
    from joblib import Parallel, delayed

    import concurrent_tasks
    import parallel_tasks

    def work(entry_vals):
    set_progress = entry_vals.get("update_status")
    partial_func = partial(concurrent_tasks.do_multi_threads, entry_vals)
    if set_progress: # EDIT
    set_progress("work", 10) # EDIT
    logging.debug("Starting multithreaded function...") # EDIT
    with ThreadPoolExecutor() as executor:
    res_1 = list(executor.map(partial_func, range(10)))

    if set_progress: # EDIT
    set_progress("work", 60) # EDIT
    logging.debug("Starting multiprocessed function...") # EDIT
    with Parallel(n_jobs=-1) as parallel_work:
    res_2 = list(parallel_work(delayed(parallel_tasks.do_multi_process)(entry_vals, i) for i in range(10)))

    return res_1, res_2

    Additional info


    This edit adds to the oversimplified code representation above: There is some functional programming that allows incrementation of a tk.ttk.Progressbar via setting a tk.IntVar which is the argument for the Progressbar parameter variable.

    app.py


    def update_status(
    progress_var,
    progress_bar,
    percent_label,
    status_label,
    lock
    ):
    def callback(section, percent):
    with lock:
    if percent is not None:
    progress_var.set(percent)
    percent_label["text"] = f"{progress_var.get()}%"
    status_label["text"] = section

    progress_bar.update_idletasks()
    percent_label.update_idletasks()
    status_label.update_idletasks()

    progress_bar.after(10000, callback)
    return callback

    Error


    Exception has occurred: RuntimeError
    main thread is not in main loop

    in start:
    long.work(entry_vals)

    in work:
    set_progress("work", 60)

    in callback:
    progress_var.set(percent)
    RuntimeError: main thread is not in main loop

    During handling of the above exception, another exception occurred:
    in start:
    button.config(state=tk.NORMAL)
    RuntimeError: main thread is not in main loop

    Continue reading...

Compartilhe esta Página