后台进程锁定GUI Python

3

我有一个使用multiprocessing库中的Process对象推送数据到GUI的后台进程,但是这个后台进程会使GUI锁定,而且被推送的更改也无法被显示。虽然对象已经被放入了队列中,但我的GUI中的更新方法并没有被正常调用。请问有什么方法可以让GUI更加频繁地更新?我的GUI是用Tkinter编写的。

我的后台进程内部有一个无限循环,因为我总是需要不断地读取USB端口以获取更多的数据,所以我的代码基本上是这样的:

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()

因此,后台线程永远不会结束,但是当我结束进程时,UI开始显示并关闭。问题可能出现在TracerAccess.py模块的设计上,因为我在从Java直接转到Python并且对Python几乎没有设计经验时开发了它。

1
你使用multiprocessing的基本想法是好的,但也许你用错了?(比如立即调用.join())。你应该发布代码,最好是一个最小、完整、可测试和可读的示例 - Martin Tournoij
@Carpetsmoker 我已经添加了一个类似的例子来说明这个问题和无限循环。 - Dean
self.checkqueue() 不应该阻塞(太久):使用 q.get_nowait() 代替 q.get()。这里有一个代码示例 - jfs
1个回答

5

multiprocess.Process 内部所做的实际上是进行了一个 fork(),这个操作会复制当前进程。你可以将它想象成:

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

p.join() 尝试将两个进程“合并”为一个。这实际上意味着:等待后台进程完成。下面是来自 .join() 函数的实际(完整)代码:

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)

请注意self._popen.wait的调用方式。
显然,这不是您想要的。
在TKinter的上下文中,您可能想要使用tk事件循环,例如像这样(Python 3,但概念也适用于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()

您不需要调用.join()方法,而是使用.after()方法,该方法会安排一个函数在n微秒后运行(如果您曾经使用过JavaScript,则可以将其视为setTimeout()),以读取队列。

根据您的bg()函数实际内容,您甚至可能根本不需要使用multiprocessing模块,只需使用.after()方法安排一个函数就足够了。

还可参见:http://tkinter.unpythonic.net/wiki/UsingTheEventLoop


我使用了你非常好的例子,并添加了更多细节,因为我正在使用.after(),但我的后台进程正在执行一些密集的操作,然后就卡住了,再也没有调用过after方法。 - Dean
@Dean 我已经更新了示例代码,包括一些更多的输出,并且现在在队列上使用 get_nowait(),因此它不会阻塞。除此之外,这对我来说似乎运行得相当不错... - Martin Tournoij
我已经用一个更小的示例复制了我的问题,并在问题的源代码中进行了更新。 - Dean
1
@Dean 一个明显的问题:在 ui.py 文件中,Application.startTrace 方法中你写的是:self.splitProc = Process(target=self.dataAbstraction.readInput()),你肯定不想在那里加上括号,因为这会使函数立即执行,而不是作为 target 进行设置(现在返回值被设置为 target,但由于该函数是一个永远不会结束的 while True 循环,所以你的脚本会冻结)。 - Martin Tournoij
现在目标函数没有被执行。这很可能是它存在的原因。 - Dean
尽管在对象之间未能传递数据,但它仍在执行。 - Dean

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接