how to update status in tkinter app when using multiprocessing on Windows, Python3.8

Viewed 121

I have a tkinter GUI in a file my_app.py and a model in another file my_model.py. The model does some calculations with multiprocessing. And MyModel has an outer loop which is outside multiprocessing. This outer loop gives the step names in the program, so I want to use this name to update a label in the GUI. So that user can see the current status. However, when the "Run" button is clicked, the GUI freezes and not responding. Only when multiprocessing finishes, can the GUI be used again, and the label just shows the last one ("Name 2"). Could you please help me?

Thanks.

I am using Python 3.8.10 on Windows system.

# my_app.py

import tkinter as tk

from mp_model import MyModel


class MyApp:
    def __init__(self):
        self._root = tk.Tk()
        self.status = tk.StringVar()
        self.status.set('Status')
        self.label = tk.Label(self._root, textvariable=self.status)
        self.btn = tk.Button(self._root, text='Run', command=self.run_model)

        self.label.pack()
        self.btn.pack()

    def run(self):
        self._root.mainloop()

    def run_model(self):
        model = MyModel(status_var=self.status)
        model.run()


if __name__ == '__main__':
    app = MyApp()
    app.run()
# my_model.py

from multiprocessing import Pool
import time
from timeit import default_timer as timer
import multiprocessing as mp

import pandas as pd


def func_for_mp(name: str, ds_value: pd.Series) -> pd.Series:
    print(f'Doing {name}.')

    res_chunk = ds_value * 2.

    time.sleep(2)

    return res_chunk


class MyModel:
    def __init__(self, status_var=None):
        self.status_var = status_var

    def run(self):
        self._outer_loop()

    def _outer_loop(self):
        names = ['Name 1', 'Name 2']
        for name in names:
            self.status_var.set(name)
            self._loop_with_mp(name)

    def _loop_with_mp(self, name: str):
        all_values = pd.Series(range(35))

        n_cpu = mp.cpu_count()
        chunk_size = int(len(all_values) / n_cpu) + 1
        ds_chunks = [
            all_values.iloc[i:i+chunk_size] for i in range(0, len(all_values), chunk_size)
        ]

        start = timer()

        with Pool(processes=n_cpu) as pool:
            args = [(name, ds_chunk) for ds_chunk in ds_chunks]
            results = pool.starmap(func_for_mp, args)

        end = timer()
        print(f'Total elapsed time: {end - start}')
1 Answers

The problem with your approach is that the Tk app freezes, while you're waiting for your model's process loop to finish executing tasks in the with Pool(processes=n_cpu) as loop context manager. Here is the complete working example on how you can fix the app:

#!/usr/bin/python3
import tkinter as tk

from model import Model

class App:

    def __init__(self):
        self._root = tk.Tk()
        self.status = tk.StringVar()
        self.status.set('Status')
        self.model = Model(status=self.status)
        self.label = tk.Label(self._root, textvariable=self.status)
        self.btn = tk.Button(self._root, text='Run', command=self.run_model)
        self.label.pack()
        self.btn.pack()

    def run(self):
        self._root.mainloop()
        print('Cleaning up...')
        self.model.cleanup()

    def run_model(self):
        self.model.run()


if __name__ == '__main__':
    app = App()
    app.run()

Note, now the Model class instance is the variable of the App instance. And this is the model implementation:

import time
import random
import multiprocessing as mp
from datetime import datetime


def task(name):
    print(f'Start task {name}')
    t_start = datetime.now()

    # Simulate a long-running task:
    time.sleep(random.randint(1, 5))

    t_elapsed = datetime.now() - t_start
    print(f'Done task {name} - Total elapsed time: {t_elapsed}')

    return name


class Model:

    def __init__(self, status):
        self.status = status
        self.pool = mp.Pool(processes=mp.cpu_count())

    def run(self):
        self._outer_loop()

    def _outer_loop(self):
        names = ['Name 1', 'Name 2']

        for name in names:
            self._loop_with_mp(name)

        self.status.set('Tasks submitted!')

    def _loop_with_mp(self, name):
        self.pool.apply_async(task, args=(name,), callback=self._task_done)

    def cleanup(self):
        self.pool.close()
        self.pool.join()

    def _task_done(self, name):
        self.status.set(f'Task {name} done!')

Here, the pool isn't created for each execution run but is initialized when the Model instance is created, so that you can submit your tasks asynchronously using the pool.apply_async method. With this approach, you can update the Tk app status label whenever the next task is finished using the _task_done callback provided to the pool.apply_async function call.

Related