使用 tkinter 在 python 中进行多处理

Posted

技术标签:

【中文标题】使用 tkinter 在 python 中进行多处理【英文标题】:multiprocessing in python using tkinter 【发布时间】:2021-10-17 11:59:43 【问题描述】:

我正在尝试了解多线程并尝试执行以下代码但收到错误。请帮助解决这个问题。

from tkinter import *
from tkinter.ttk import *
import tkinter as tk
import datetime
import multiprocessing

process1 = None


class App:
    def __init__(self):
        self.root = Tk()
        self.top_frame = tk.Frame(self.root, height=50, pady=3)
        self.selectFile = tk.Button(self.top_frame, text="Start", activebackground="blue",
                                    command=lambda: self.create_process()).pack()
        self.progressbar_frame = tk.Frame(self.root)
        self.pgbar = Progressbar(self.progressbar_frame, length=125, orient=HORIZONTAL, mode="indeterminate")
        self.pgbar.pack()

        self.top_frame.pack()
        self.root.mainloop()

    def calculate_data(self):
        a = datetime.datetime.now()
        i = 0
        while i < 100000000:
            i+=1
        print(i)
        b = datetime.datetime.now()
        print(b - a)

    def create_process(self):
        #self.pgbar_start()
        global process1
        process1 = multiprocessing.Process(target=self.calculate_data, args=())
        process2 = multiprocessing.Process(target=self.pgbar_start, args=())
        process1.start()
        process2.start()
        self.periodic_call()

    def pgbar_start(self):
        self.progressbar_frame.pack()
        self.pgbar.start(10)

    def pgbar_stop(self):
        self.pgbar.stop()
        self.progressbar_frame.pack_forget()

    def periodic_call(self):
        if process1.is_alive():
            self.pgbar.after(1000, self.periodic_call)
        else:
            self.pgbar_stop()


if __name__ == "__main__":
    app = App()

我收到以下错误:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Program Files\Python37\lib\tkinter\__init__.py", line 1705, in __call__
    return self.func(*args)
  File "C:/Python Automation/Practice/multi_processing.py", line 15, in <lambda>
    command=lambda: self.create_process()).pack()
  File "C:/Python Automation/Practice/multi_processing.py", line 37, in create_process
    process1.start()
  File "C:\Program Files\Python37\lib\multiprocessing\process.py", line 112, in start
    self._popen = self._Popen(self)
  File "C:\Program Files\Python37\lib\multiprocessing\context.py", line 223, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
  File "C:\Program Files\Python37\lib\multiprocessing\context.py", line 322, in _Popen
    return Popen(process_obj)
  File "C:\Program Files\Python37\lib\multiprocessing\popen_spawn_win32.py", line 89, in __init__
    reduction.dump(process_obj, to_child)
  File "C:\Program Files\Python37\lib\multiprocessing\reduction.py", line 60, in dump
    ForkingPickler(file, protocol).dump(obj)
TypeError: can't pickle _tkinter.tkapp objects
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\Program Files\Python37\lib\multiprocessing\spawn.py", line 105, in spawn_main
    exitcode = _main(fd)
  File "C:\Program Files\Python37\lib\multiprocessing\spawn.py", line 115, in _main
    self = reduction.pickle.load(from_parent)
EOFError: Ran out of input

请帮助我了解我做错了什么。我的目标是在带有后台进程的 tkinter 窗口中运行进度条。进度条应该运行顺畅。

【问题讨论】:

您不能将您的应用程序传递给生成的进程。您必须找到一种解决方案,在主进程中处理应用程序,而在新进程中单独处理工作负载。 你为什么使用新进程而不是新线程?这确实很重要,因为您应该只对所有 tkinter 调用使用 1 个线程。处理 tkinter 时,多处理是不可能的 你可以把计算放到一个单独的线程或进程中,但是所有的 GUI 东西都必须在主进程的主线程中完成。 只有在另一个进程中绝对没有 tkinter 代码的情况下,多处理才能工作。 Tkinter 对象不能跨越进程边界。 @TheLizzard first of multiprocessing 并不是真的不可能,您仍然可以与进程通信而无需从该进程调用tkinter 的东西(与线程相同)但至少有一个线程不起作用的情况,至少有点。线程使用与整个进程相同的资源,因此如果您在主进程 tkinter 和一个或多个线程或多个线程消耗相同的资源并且经常执行它可能会使 tkinter 的这些资源更少并且它可能变得非常滞后,所以你可以将这个东西扩展到拥有自己资源的多个进程 【参考方案1】:

也许我误解了某事,但我很确定您是在询问多处理,或者至少它在您的代码中,所以这里是如何结合 tkinter(代码 cmets 中的解释)(以及“结合”我意味着tkinter 必须始终位于主进程和一个线程中,因此在这种情况下,诸如“计算”之类的其他内容将被移动到其他线程或进程中):

# import what is needed and don't ask about threading yet that will be explained
# (pretty useless comment anyways)
from tkinter import Tk, Button, Label, DoubleVar
from tkinter.ttk import Progressbar
from multiprocessing import Process, Queue
from threading import Thread
from queue import Empty
from time import perf_counter, sleep


# create main window also inherit from `Tk` to make the whole thing a bit easier
# because it means that `self` is the actual `Tk` instance
class MainWindow(Tk):
    def __init__(self):
        Tk.__init__(self)
        # prepare the window, some labels are initiated but not put on the screen
        self.geometry('400x200')

        self.btn = Button(
            self, text='Calculate', command=self.start_calc
        )
        self.btn.pack()

        self.info_label = Label(self, text='Calculating...')

        # progressbar stuff
        self.progress_var = DoubleVar(master=self, value=0.0)
        self.progressbar = Progressbar(
            self, orient='horizontal', length=300, variable=self.progress_var
        )

        # create a queue for communication
        self.queue = Queue()

    # the method to launch the whole process and start the progressbar
    def start_calc(self):
        self.info_label.pack()
        self.progressbar.pack()
        Process(target=calculation, args=(self.queue, ), daemon=True).start()
        self.update_progress()

    # this function simply updates the `DoubleVar` instance
    # that is assigned to the Progressbar so basically makes
    # the progressbar move
    def update_progress(self):
        try:
            data = self.queue.get(block=False)
        except Empty:
            pass
        else:
            # if the process has finished stop this whole thing (using `return`)
            if data == 'done':
                self.info_label.config(text='Done')
                self.progress_var.set(100)
                return
            self.progress_var.set(data)
        finally:
            self.after(100, self.update_progress)


# interestingly this function couldn't be a method of the class
# because the class was inheriting from `Tk` (at least I think that is the reason)
# and as mentioned `tkinter` and multiprocessing doesn't go well together
def calculation(queue):
    # here is the threading this is important because the below
    # "calculation" is super quick while the above update loop runs only every
    # 100 ms which means that the Queue will be full and this process finished
    # before the progressbar will show that it is finished
    # so this function in a thread will only put stuff in the queue
    # every 300 ms giving time for the progressbar to update
    # if the calculation takes longer per iteration this part is not necessary
    def update_queue():
        while True:
            sleep(0.3)
            queue.put(i / range_ * 100)  # put in percentage as floating point where 100 is 100%
    # here starting the above function again if the calculations per iteration
    # take more time then it is fine to not use this
    Thread(target=update_queue).start()
    # starts the "calculation"
    start = perf_counter()
    range_ = 100_000_000
    for i in range(range_):
        pass
    finish = perf_counter()
    # put in the "sentinel" value to stop the update
    # and notify that the calculation has finished
    queue.put('done')
    # could actually put the below value in the queue to and
    # handle so that this is show on the `tkinter` window
    print((finish - start))


# very crucial when using multiprocessing always use the `if __name__ == "__main__":` to avoid
# recursion or sth because the new processes rerun this whole thing so it can end pretty badly
# there is sth like a fail safe but remember to use this anyways (a good practice in general)
if __name__ == '__main__':
    # as you can see then inheriting from `Tk` means that this can be done too
    root = MainWindow()
    root.mainloop()

非常重要(建议,但您确实需要遵循,尤其是在这种情况下,我已经看到至少有两个人在从 tkintertkinter.ttk 导入所有内容时犯了这个错误): 我强烈建议在导入某些内容时不要使用通配符 (*),您应该导入您需要的内容,例如from module import Class1, func_1, var_2 等等或导入整个模块:import module 然后你也可以使用别名:import module as md 或类似的东西,关键是不要导入所有东西,除非你真的知道你在做什么;名称冲突是问题所在。

编辑:确实是小事,但将daemon=True 添加到Process 实例中,以便在主进程退出时关闭进程(据我所知,这并不是退出进程的最佳方式(线程顺便说一句有同样的事情)但不知道它是否真的那么糟糕(我想也取决于进程的作用,但例如它可能无法正确关闭文件或类似的东西,但如果你不写入文件或者在最坏的情况下,它可能就像再次运行进程以重新获得一些丢失的进度一样简单(在当前示例中,如果关闭窗口或使用任务关闭整个程序,进程只能突然退出经理或某事)))

EDIT2(相同的代码只是删除了所有的 cmets 以使其不那么笨重):

from tkinter import Tk, Button, Label, DoubleVar
from tkinter.ttk import Progressbar
from multiprocessing import Process, Queue
from threading import Thread
from queue import Empty
from time import perf_counter, sleep


class MainWindow(Tk):
    def __init__(self):
        Tk.__init__(self)

        self.geometry('400x200')

        self.btn = Button(
            self, text='Calculate', command=self.start_calc
        )
        self.btn.pack()

        self.info_label = Label(self, text='Calculating...')

        self.progress_var = DoubleVar(master=self, value=0.0)
        self.progressbar = Progressbar(
            self, orient='horizontal', length=300, variable=self.progress_var
        )

        self.queue = Queue()

    def start_calc(self):
        self.info_label.pack()
        self.progressbar.pack()
        Process(target=calculation, args=(self.queue, ), daemon=True).start()
        self.update_progress()

    def update_progress(self):
        try:
            data = self.queue.get(block=False)
        except Empty:
            pass
        else:
            if data == 'done':
                self.info_label.config(text='Done')
                self.progress_var.set(100)
                return
            self.progress_var.set(data)
        finally:
            self.after(100, self.update_progress)


def calculation(queue):
    def update_queue():
        while True:
            sleep(0.3)
            queue.put(i / range_ * 100)
    Thread(target=update_queue).start()
    start = perf_counter()
    range_ = 100_000_000
    for i in range(range_):
        pass
    finish = perf_counter()
    queue.put('done')
    print((finish - start))


if __name__ == '__main__':
    root = MainWindow()
    root.mainloop()

如果您有任何其他问题,请提出!

【讨论】:

以上是关于使用 tkinter 在 python 中进行多处理的主要内容,如果未能解决你的问题,请参考以下文章

使用 Tkinter 和 Python 在 Mac OS X 中进行惯性滚动

使用 Pyglet 进行多处理会打开一个新窗口

Python Chapter 9: 使用Tkinter进行GUI程序设计 Part 2

python tkinter Label 图形

使用 Python 和 tkinter 进行多处理

Python Tkinter模块 Grid布局管理器参数详解