0

I have functioning code that displays data in a GUI which is periodically updated with new information downloaded from the web. (The base code for the threaded approach was sourced from https://www.oreilly.com/library/view/python-cookbook/0596001673/ch09s07.html) I am using a threaded solution so as to improve blocking IO issues (IO code not included in the simplified code example below, as the IO does not appear to be the problem). The code runs fine if I run it as a single instance. However, it would be most convenient if I could use multiprocessing to run several instances of the code in parallel, using a different input list for each instance. When I try to implement the multiprocessing version, each separate process hangs during the attempt to create the root window: "window = tk.Tk()". Here is the working single instance version:

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)

So, like I said, the above works - a single tkinter GUI is displayed appropriately without errors. However, if I attempt to implement multiprocessing with the following code below, the code spawns two processes as expected, and as documented by the printout of pid. However, each process prints the 'list2Get' (in runGUIthread), and then there is nothing else. There is no error message and the python code seems to have exited as there is no persistent process listed in the system activity monitor. Presumably the code is "hanging"/ exiting at the line "window = tk.TK()", as the line "print('type of window=',type(window))" is never executed:

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)

I am not experienced with multiprocessing, so perhaps I have implemented it incorrectly. I tried using multiprocessing.Pool(), with the same results. I have not been able to find info indicating that tkinter can't spawn multiple GUI displays in the same program. In fact I found an instance of somebody accidentally spawning multiple GUI's, although this appears to be with Python 3.8 using concurrent.futures.ProcessPoolExecutor (Concurrent.futures opens new windows in tkinter instead of running the function). I am currently on Python 3.7, and was hoping not to have to reinstall a new enviroment to make this multiprocessing code work, although perhaps that is necessary...?

Other info: using python 3.7.6, tkinter 8.6.8, Eclipse 4.11.0, macOS10.13.6.

Any help appreciated.

gymshoe
  • 7,495
  • 5
  • 20
  • 21

2 Answers2

3

You cannot use tkinter code across multiple processes. At least, you can't run the same tkinter code. It simply isn't designed to be used that way. When you create a root window, underneath the covers a tcl interpreter is created, and this interpreter can't be pickled or shared between processes, and doesn't use python's global interpreter lock.

In short, all of your GUI code needs to be in a single thread in a single process.

The following answer is a slightly better explanation, written by one of the developers on the Tcl core team: https://stackoverflow.com/a/38767665/7432. Here is the opening paragraph of that answer:

Each Tcl interpreter object (i.e., the context that knows how to run a Tcl procedure) can only be safely used from the OS thread that creates it. This is because Tcl doesn't use a global interpreter lock like Python, and instead makes extensive use of thread-specific data to reduce the number of locks required internally. (Well-written Tcl code can take advantage of this to scale up very large on suitable hardware.)

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Thanks for the response. I understand that you can't run tkinter code "across multiple processes". But in my case I don't want to run the tkinter code "across multiple processes" but rather I want to run a completely separate instance of tkinter in EACH process (with NO interaction between any of the processes or "across" the processes). Does that make sense? Is there a way to construct that? It seems like in the SO problem referenced above ("Concurrent.futures opens new windows in tkinter instead of running the function"), the OP was complaining about spawning multiple GUI'S... – gymshoe Jun 28 '20 at 20:47
  • @gymshoe: as long as each tkinter instance is completely separate - no sharing of widget, tkinter variables, etc, yes, it should be possible. – Bryan Oakley Jun 28 '20 at 21:15
  • Well that's encouraging. I thought in my code I had adequtely provided for a separate process for each Tk() instance, so not sure why that isn't working... Oh well, more to learn/try. Maybe I will have to try using ProcessPoolExecutor in python 3.8... – gymshoe Jun 28 '20 at 21:30
1

I found that this has been reported as part of a bug related to tkinter, python, and macOSX: https://bugs.python.org/issue33111. (The bug was reported for python 2.7 and 3.6.4 and OSX 10.11.6, but apparently is still a problem with python 3.7.6 and OSX 10.13.6.)

However, there is a partial workaround (also reported at the same site), which for my case seems to work perfectly:

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)

The result is the spawning of multiple GUIs, one for each process.

Per the bug report, in python 3.8, the default start method for multiprocessing when run on MacOS has been changed and is now "spawn" instead of "fork", so the problem will not reveal itself (unless you change the start method to be "fork", in which case the code will fail).

gymshoe
  • 7,495
  • 5
  • 20
  • 21