使用 tkinter 进行多处理不会产生多个 GUI

Posted

技术标签:

【中文标题】使用 tkinter 进行多处理不会产生多个 GUI【英文标题】:multiprocessing with tkinter won't spawn multiple GUI's 【发布时间】:2020-10-18 22:57:58 【问题描述】:

我有在 GUI 中显示数据的功能代码,该 GUI 会使用从网络下载的新信息定期更新。 (线程方法的基本代码来自https://www.oreilly.com/library/view/python-cookbook/0596001673/ch09s07.html)我正在使用线程解决方案来改善阻塞 IO 问题(IO 代码未包含在下面的简化代码示例中,因为 IO 似乎不是问题)。如果我将其作为单个实例运行,则代码运行良好。但是,如果我可以使用多处理并行运行代码的多个实例,对每个实例使用不同的输入列表,那将是最方便的。当我尝试实现多处理版本时,每个单独的进程在尝试创建根窗口期间挂起:“window = tk.Tk()”。这是工作的单实例版本:

import threading
import random
import tkinter as tk
import random
import queue #Queue
import multiprocessing
import psutil

class GuiPartBase:    
    def __init__(self, master, queue, myList, endCommand):            
        self.queue = queue
        # Set up the GUI
        a = Label(master, text="Test Tkinter Display!")
        a.pack()
        ## etc
    
    def processIncoming(self):
    """Handle all messages currently in the queue, if any."""
        while self.queue.qsize():
            try:
                result = (self.queue.get(0))
                ## do stuff with incoming data...                
                print('result =', result)
            except queue.Empty:
                # just on general principles...
                pass

class ThreadedClientBase:
    """
    Launch the main part of the GUI and the worker thread. periodicCall and
    endApplication could reside in the GUI part, but putting them here
    means that you have all the thread controls in a single place.
    """
    def __init__(self, master, mylist):
        """
        Start the GUI and the asynchronous threads. We are in the main
        (original) thread of the application, which will later be used by
        the GUI as well. We spawn a new thread for the worker (I/O).
        """
        self.master = master
        self.mylist = mylist

        # Create the queue
        self.queue = queue.Queue()

        # Set up the GUI part
        self.gui = GuiPartBase(self.master, self.queue, mylist, self.endApplication)
    
        # Set up the thread to do asynchronous I/O
        # More threads can also be created and used, if necessary
        self.running = 1
        self.thread1 = threading.Thread(target=self.workerThread1)
        self.thread1.start()

        # Start the periodic call in the GUI to check if the queue contains
        # anything
        self.periodicCall()

    def periodicCall(self):
        """
        Check every 200 ms if there is something new in the queue.
        """
        self.gui.processIncoming()
        if not self.running:
            # This is the brutal stop of the system. You may want to do
            # some cleanup before actually shutting it down.
            import sys
            sys.exit(1)
        self.master.after(200, self.periodicCall)

    def workerThread1(self):
        """
        This is where we handle the asynchronous I/O. For example, it may be
        a 'select(  )'. One important thing to remember is that the thread has
        to yield control pretty regularly, by select or otherwise.
        """               
        while self.running:
            #  simulate asynchronous I/O, 
            time.sleep(rand.random() * 1.5)
            msg = rand.random()
            self.queue.put(msg)

    def endApplication(self):
        self.running = 0

def runGUIthread(threadedList2Get):
    print('entering runGUIthread...')
    print('list2Get = ', threadedList2Get)
    window = tk.Tk()    
    print('type of window = ', type(window))
    print('window = ', window)
    client = ThreadedClientBase(window, threadedList2Get)
    print('type of client = ', type(client))
    print('client = ', client)

    window.mainloop() 

if __name__ == '__main__':
    rand = random.Random()

    testList2a = ['abc','def','geh']
    testList2b = ['xyz', 'lmn', 'opq']
    allLists = [testList2a,testList2b]
    runGUIthread(testList2a)

所以,就像我说的,上面的工作 - 一个单一的 tkinter GUI 正确显示,没有错误。但是,如果我尝试使用下面的代码实现多处理,则代码会按预期生成两个进程,并由 pid 的打印输出记录。但是,每个进程都会打印“list2Get”(在 runGUIthread 中),然后就没有其他内容了。没有错误消息并且 python 代码似乎已经退出,因为系统活动监视器中没有列出持久进程。大概代码是“挂起”/在“window = tk.TK()”行退出,因为“print('type of window=',type(window))”行永远不会执行:

if __name__ == '__main__':
    rand = random.Random()

    testList2a = ['abc','def','geh']
    testList2b = ['xyz', 'lmn', 'opq']
    allLists = [testList2a,testList2b]
    #runGUIthread(testList2a)
    for list in allLists:
        p = multiprocessing.Process(target=runGUIthread, args=(list,))
        p.start()
        ps = psutil.Process(p.pid)
        print('pid = ', ps)

    #with multiprocessing.Pool(processes=2) as pool:
    #    pool.map(runGUIthread, allLists)

我对多处理没有经验,所以也许我实现它不正确。我尝试使用 multiprocessing.Pool(),结果相同。 我无法找到表明 tkinter 无法在同一程序中生成多个 GUI 显示的信息。事实上,我发现有人意外生成了多个 GUI,尽管这似乎是 Python 3.8 使用 concurrent.futures.ProcessPoolExecutor (Concurrent.futures opens new windows in tkinter instead of running the function)。我目前使用的是 Python 3.7,并且希望不必重新安装新环境来使这个多处理代码工作,尽管这可能是必要的......?

其他信息:使用 python 3.7.6、tkinter 8.6.8、Eclipse 4.11.0、macOS10.13.6。

任何帮助表示赞赏。

【问题讨论】:

【参考方案1】:

您不能跨多个进程使用 tkinter 代码。至少,您不能运行相同的 tkinter 代码。它根本不是为了那样使用而设计的。当你创建一个根窗口时,会在底层创建一个 tcl 解释器,这个解释器不能被pickle或在进程之间共享,并且不使用 python 的全局解释器锁。

简而言之,您的所有 GUI 代码都需要在单个进程中的单个线程中。

以下答案是一个稍微好一点的解释,由 Tcl 核心团队的一位开发人员编写:https://***.com/a/38767665/7432。这是该答案的开头段落:

每个 Tcl 解释器对象(即知道如何运行 Tcl 过程的上下文)只能从创建它的 OS 线程安全地使用。这是因为 Tcl 没有像 Python 那样使用全局解释器锁,而是广泛使用线程特定的数据来减少内部所需的锁数量。 (编写良好的 Tcl 代码可以利用这一点在合适的硬件上进行非常大的扩展。)

【讨论】:

感谢您的回复。我了解您不能“跨多个进程”运行 tkinter 代码。但在我的情况下,我不想“跨多个进程”运行 tkinter 代码,而是我想在每个进程中运行一个完全独立的 tkinter 实例(任何进程之间或“跨”进程之间没有交互) .那有意义吗?有没有办法构建它?似乎在上面提到的 SO 问题中(“Concurrent.futures 在 tkinter 中打开新窗口而不是运行函数”),OP 抱怨产生多个 GUI'S ... @gymshoe:只要每个 tkinter 实例是完全独立的——不共享小部件、tkinter 变量等,是的,应该是可能的。 这令人鼓舞。我认为在我的代码中,我已经为每个 Tk() 实例提供了一个单独的进程,所以不知道为什么这不起作用......哦,好吧,还有更多要学习/尝试。也许我将不得不尝试在 python 3.8 中使用 ProcessPoolExecutor ...【参考方案2】:

我发现这已被报告为与 tkinter、python 和 macOSX 相关的错误的一部分:https://bugs.python.org/issue33111。 (python 2.7 和 3.6.4 以及 OSX 10.11.6 报告了该错误,但显然 python 3.7.6 和 OSX 10.13.6 仍然存在问题。)

但是,有一个部分解决方法(也在同一站点上报告),对于我的情况来说,这似乎工作得很好:

import multiprocessing
multiprocessing.set_start_method("spawn", force=True)
...
... other code same as initial ...
...

if __name__ == '__main__':
    testList2a = ['abc','def','geh']
    testList2b = ['xyz', 'lmn', 'opq']
    allLists = [testList2a,testList2b]
    with multiprocessing.Pool(processes=2) as pool:
       pool.map(runGUIthread, allLists)

结果是生成了多个 GUI,每个进程一个。

根据错误报告,在 python 3.8 中,在 MacOS 上运行时多处理的默认启动方法已更改,现在是“spawn”而不是“fork”,因此问题不会出现(除非您更改启动方法为“fork”,在这种情况下代码将失败)。

【讨论】:

以上是关于使用 tkinter 进行多处理不会产生多个 GUI的主要内容,如果未能解决你的问题,请参考以下文章

使用 tkinter 和多处理可执行创建多个窗口

在多处理之前调用 Tkinter 会导致多个窗口?

使用 Python 和 tkinter 进行多处理

使用 tkinter 在 python 中进行多处理

使用 Tkinter 和 Selenium 进行多处理

Python多处理-TypeError:无法腌制'_tkinter.tkapp'对象