I'm looking for a way to track a multiprocessing task with Tkinter progress bar. This is something that can be done very straightforwardly with tqdm
for display in the terminal.
Instead of using tqdm
I'd like to use ttk.Progressbar
, but all attempts I have made at this, the tasks block on trying to update the progressbar (e.g. using update_idletasks and similar). Below is a template of the kind of solution I'm looking for:
import time
from multiprocessing import Pool
from tqdm import tqdm
import tkinter as tk
import tkinter.ttk as ttk
def task(x):
time.sleep(0.1)
return x * x
def start_task():
num_processes = 12
num_tasks = 100
with Pool(processes=num_processes) as pool:
with tqdm(total=num_tasks, desc="Processing") as pbar:
def update_progress(_):
# <Insert update to tk progress bar here>
pbar.update(1)
for i in range(num_tasks):
pool.apply_async(task, args=(i,), callback=update_progress)
pool.close()
pool.join()
if __name__ == "__main__":
root = tk.Tk()
root.title("Task Progress")
progress_bar = ttk.Progressbar(root, maximum=100, length=300)
progress_bar.pack(pady=20)
button = tk.Button(text="Start", command=start_task)
button.pack(fill="x", padx=10, pady=10)
root.mainloop()
In the solution I'd also like to get the output of the task (in this case a list of x*x).
If another multiprocessing structure would work better please feel free to adjust (pool just seemed the simplest for demonstration).
This is a question that has been asked on Stack Overflow before, but all the previous answers I've found have not been minimal examples and I didn't find them very helpful.
Calling pool.join()
blocks the main thread until all the tasks are done, which causes Tkinter to hang. To get around this, you can call start_task
in a thread. Running the thread with .start()
(instead of .join()
) will make it run in the background so it doesn't block the main thread. You can pass lists to the thread for keeping track of progress and results. These lists can be updated by the update_progress
function, which takes the return value of task
as an argument.
To update the progress bar I've added the update_bar
function. This removes each value from progress
using pop
, then increases the progress bar by that amount with step
. This is run every 100ms while the thread is alive. Once it finishes, use_results
is called, which can then do something with the result (I've made it display the sum of values as an example).
Finally, I've introduced start_start_task
(which could probably be named better but you get the idea). This is called when the button is clicked and initialises the lists, starts the thread and calls the update_bar
function.
import time
from multiprocessing import Pool
from threading import Thread
import tkinter as tk
import tkinter.ttk as ttk
def task(x):
time.sleep(0.1)
return x*x
def start_task(progress, results):
num_processes = 12
num_tasks = 100
with Pool(processes=num_processes) as pool:
def update_progress(result):
progress.append(1)
results.append(result)
for i in range(num_tasks):
pool.apply_async(task, args=(i,), callback=update_progress)
pool.close()
pool.join()
def update_bar(thread, progress, results):
# check for progress
while progress:
progress_bar.step(progress.pop())
# if the tasks are still running then call the update function after 100ms
if thread.is_alive():
root.after(100, lambda: update_bar(thread, progress, results))
# if the tasks are done call the result handler
else:
use_results(results)
def use_results(results):
# do whatever you want with the result
result_label.config(text = f"Sum of results: {sum(results)}")
def start_start_task():
progress = []
results = []
thread = Thread(target=start_task, args=(progress, results))
thread.start()
update_bar(thread, progress, results)
if __name__ == "__main__":
root = tk.Tk()
root.title("Task Progress")
progress_bar = ttk.Progressbar(root, maximum=100, length=300)
progress_bar.pack(pady=20)
button = tk.Button(root, text="Start", command=start_start_task)
button.pack(fill="x", padx=10, pady=10)
result_label = tk.Label(root, text="Press Start to get result")
result_label.pack()
root.mainloop()