使用 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()
非常重要(建议,但您确实需要遵循,尤其是在这种情况下,我已经看到至少有两个人在从 tkinter
和 tkinter.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 中进行惯性滚动