在 Windows,Python3.8 上使用多处理时如何更新 tkinter 应用程序中的状态

Posted

技术标签:

【中文标题】在 Windows,Python3.8 上使用多处理时如何更新 tkinter 应用程序中的状态【英文标题】:how to update status in tkinter app when using multiprocessing on Windows, Python3.8 【发布时间】:2021-10-22 11:54:48 【问题描述】:

我在文件 my_app.py 中有一个 tkinter GUI,在另一个文件 my_model.py 中有一个模型。该模型使用多处理进行一些计算。 MyModel 有一个外部循环,它在多处理之外。这个外部循环给出了程序中的步骤名称,所以我想使用这个名称来更新 GUI 中的标签。这样用户就可以看到当前状态。但是,当单击“运行”按钮时,GUI 冻结并且没有响应。只有当多处理完成后,才能再次使用 GUI,并且标签只显示最后一个(“名称 2”)。你能帮帮我吗?

谢谢。

我在 Windows 系统上使用 Python 3.8.10。

# 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')

【问题讨论】:

_loop_with_mp 的单次运行需要多长时间?还有Name 2 没有Name 1 显示的原因是因为代码执行不会回到tkinter 的主循环,所以它不能自我更新。尝试使用 tkinter 循环重写 for 循环,例如 this MyModel 有一个在多处理之外的外部循环。我想这就是问题所在。如果主循环中的循环未完成,则主循环将无法继续,因此您的 GUI 将冻结。 【参考方案1】:

您的方法的问题是 Tk 应用程序冻结,而您正在等待模型的流程循环在 with Pool(processes=n_cpu) as loop 上下文管理器中完成执行任务。这是有关如何修复应用程序的完整工作示例:

#!/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()

注意,现在Model 类实例是App 实例的变量。这是模型实现:

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!')

这里,池不是为每次执行运行创建的,而是在创建Model 实例时初始化,因此您可以使用pool.apply_async 方法异步提交任务。使用这种方法,您可以在下一个任务完成时使用提供给 pool.apply_async 函数调用。

【讨论】:

以上是关于在 Windows,Python3.8 上使用多处理时如何更新 tkinter 应用程序中的状态的主要内容,如果未能解决你的问题,请参考以下文章

windows7 上安装python3.8步骤

windows上python3.8安装virtualenv遇到的一些问题

使用 python 3.8 在 windows 10 上安装 django 频道和扭曲的问题

Flask 学习-59.解决celery 在windows 上接收任务不执行的问题

Python3.8.0安装(windows)

在Windows 10上安装Twisted时出错,Python 3.8.0