Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Background Process Locking up GUI Python

I have a background Process (using Process from multiprocessing) that is pushing objects to my GUI, however this background process keeps locking up the GUI and the changes being pushed are never being displayed. The objects are being put in to my queue, however the update method in my GUI isn't being called regularly. What can I do make the GUI update more regularly? My GUI is written in Tkinter.

My background process has a infinite loop within it because I always need to keep reading the USB port for more data, so basically my code looks like this:

TracerAccess.py

import usb
from types import *
import sys
from multiprocessing import Process, Queue
import time


__idVendor__ = 0xFFFF
__idProduct__ = 0xFFFF

END_POINT = 0x82

def __printHEXList__(list):
    print ' '.join('%02x' % b for b in list)

def checkDeviceConnected():
    dev = usb.core.find(idVendor=__idVendor__, idProduct=__idProduct__)
    if dev is None:
        return False
    else:
        return True

class LowLevelAccess():
    def __init__(self):
        self.rawIn = []
        self.tracer = usb.core.find(idVendor=__idVendor__, idProduct=__idProduct__)
        if self.tracer is None:
            raise ValueError("Device not connected")
        self.tracer.set_configuration()

    def readUSB(self):
        """
        This method reads the USB data from the simtracer.
        """
        try:
            tmp = self.tracer.read(END_POINT, 10000,None, 100000).tolist()
            while(self.checkForEmptyData(tmp)):
                tmp = self.tracer.read(END_POINT, 10000,None, 100000).tolist()
            self.rawIn = tmp
        except:
            time.sleep(1)
            self.readUSB()

    def checkForEmptyData(self, raw):
        if(len(raw) == 10 or raw[10] is 0x60 or len(raw) == 11):
            return True
        else:
            return False

class DataAbstraction:
    def __init__(self, queue):
        self.queue = queue
        self.lowLevel = LowLevelAccess()
    def readInput(self):
        while True:
            self.lowLevel.readUSB()
            raw = self.lowLevel.rawIn
            self.queue.put(raw)

ui.py

from Tkinter import *
import time
import TracerAccess as io
from multiprocessing import Process, Queue
from Queue import Empty
from math import ceil

def findNumberOfLines(message):
    lines = message.split("\n")
    return len(lines)


class Application(Frame):
    def addTextToRaw(self, text, changeColour=False, numberOfLines=0):
        self.rawText.config(state=NORMAL)
        if changeColour is True:
            self.rawText.insert(END,text, 'oddLine')
        else:
            self.rawText.insert(END,text)
        self.rawText.config(state=DISABLED)

    def updateRaw(self, text):
        if(self.numberOfData() % 2 is not 0):
            self.addTextToRaw(text, True)
        else:
            self.addTextToRaw(text)
    def startTrace(self):
        self.dataAbstraction = io.DataAbstraction(self.queue)
        self.splitProc = Process(target=self.dataAbstraction.readInput())
        self.stopButton.config(state="normal")
        self.startButton.config(state="disabled")
        self.splitProc.start()

    def pollQueue(self):
        try:
            data = self.queue.get(0)
            self.dataReturned.append(data)
            self.updateRaw(str(data).upper())
            self.rawText.tag_config("oddLine", background="#F3F6FA")
        except Empty:
            pass
        finally:
            try:
                if(self.splitProc.is_alive() is False):
                    self.stopButton.config(state="disabled")
                    self.startButton.config(state="normal")
            except AttributeError:
                pass
            self.master.after(10, self.pollQueue)

    def stopTrace(self):
        self.splitProc.join()
        self.stopButton.config(state="disabled")
        self.startButton.config(state="normal")

    def createWidgets(self):
        self.startButton = Button(self)
        self.startButton["text"] = "Start"
        self.startButton["command"] = self.startTrace
        self.startButton.grid(row = 0, column=0)

        self.stopButton = Button(self)
        self.stopButton["text"] = "Stop"
        self.stopButton["command"] = self.stopTrace
        self.stopButton.config(state="disabled")
        self.stopButton.grid(row = 0, column=1)

        self.rawText = Text(self, state=DISABLED, width=82)
        self.rawText.grid(row=1, columnspan=4)


    def __init__(self, master):
        Frame.__init__(self, master)
        self.queue = Queue()
        self.master.after(10, self.pollQueue)
        self.pack()
        self.dataReturned = []
        self.createWidgets()

    def numberOfData(self):
        return len(self.dataReturned)

Main.py

import ui as ui

if __name__ == "__main__":
    root = Tk()
    root.columnconfigure(0,weight=1)
    app = ui.Application(root)
    app.mainloop()

So the background thread never finishes, however when I end the process the UI starts to be displayed before closing. The problem could have appeared because of my design for the TracerAccess.py module as I developed this after moving straight form java and little to no design experience for python.

like image 899
Dean Avatar asked Mar 03 '26 03:03

Dean


1 Answers

What multiprocess.Process does, internally, is really a fork(), which effectively duplicated your process. You can perhaps visualize it as:

                  / ["background" process] -------------\
[main process] --+                                       +-- [main process]
                  \ [main process continued] -----------/

p.join() attempts to "join" the two processes back to one. This effectively means: waiting until the background process is finished. Here's the actual (full) code from the .join() function:

def join(self, timeout=None):
    '''
    Wait until child process terminates
    '''
    assert self._parent_pid == os.getpid(), 'can only join a child process'
    assert self._popen is not None, 'can only join a started process'
    res = self._popen.wait(timeout)
    if res is not None:
        _current_process._children.discard(self)

Note how self._popen.wait is called.

This is obviously not what you want.

What you probably want, in the context of TKinter, is use the tk event loop, for example like this (Python 3, but the concept also works on Python 2)

from multiprocessing import Process, Queue
import time, tkinter, queue, random, sys

class Test:
    def __init__(self, root):
        self.root = root
        txt = tkinter.Text(root)
        txt.pack()

        self.q = Queue()

        p = Process(target=self.bg)
        p.start()

        self.checkqueue()
        print('__init__ done', end=' ')

    def bg(self):
        print('Starting bg loop', end=' ')
        n = 42
        while True:
            # Burn some CPU cycles
            (int(random.random() * 199999)) ** (int(random.random() * 1999999))
            n += 1
            self.q.put(n)
            print('bg task finished', end=' ')

    def checkqueue(self):
        try:
            print(self.q.get_nowait(), end=' ')
        except queue.Empty:
            print('Queue empty', end=' ')

        sys.stdout.flush()

        # Run myself again after 1 second
        self.root.after(1000, self.checkqueue)


root = tkinter.Tk()
Test(root)
root.mainloop()

You don't call the .join(), and instead use the .after() method, which schedules a function to run after n microseconds (if you've ever used Javascript, then think setTimeout()) to read the queue.

Depending on the actual content of your bg() function, you may not event need multiprocesing at all, just scheduling a function with .after() may be enough.

Also see: http://tkinter.unpythonic.net/wiki/UsingTheEventLoop

like image 192
Martin Tournoij Avatar answered Mar 05 '26 15:03

Martin Tournoij