从多进程计算更新TKinter GUI

3
我正在为一个Python模拟器创建GUI。GUI提供了设置模拟和运行模拟的工具。在模拟运行时,我想向GUI传递进度信息,并在我的simulation_frame中显示在Label上。由于需要使用多进程来运行模拟,我使用Queue将更新后的信息传递回GUI。
我的设置方式是,运行模拟会阻塞Tk主循环,因为我需要能够在调用结束时关闭我的Pool。我调用update_idletasks()来强制GUI更新进度信息。
这对我来说似乎是一种不优雅且潜在风险的解决方案。此外,虽然它在Ubuntu上运行良好,但在Windows XP上似乎无法正常工作--窗口在运行一秒钟左右后变为空白。我可以通过调用update()而不是update_idletasks()来使其在Windows中工作,但这对我来说甚至更糟糕。
有更好的解决方案吗?
相关代码:
sims = []
queues = []
svars = []
names = []
i = 0
manager = mp.Manager()
for config in self.configs:
    name, file, num = config.get()
    j = 0
    for _ in range(num):
        #progress monitor label
        q = manager.Queue()
        s_var = StringVar()
        label = Label(self.sim_frame, textvariable = s_var, bg = "white")
        s_var.set("%d: Not Started"%i)
        label.grid(row = i, column = 0, sticky = W+N)
        self.sim_labels.append(label)
        queues.append(q)
        svars.append(s_var)
        names.append("%s-%d"%(name, j))
        sims.append(("%s-%d"%(name, j),file, data, verbose, q))
        i += 1
        j += 1
self.update()

# The progress tracking is pretty hacky.

pool = mp.Pool(parallel)
num_sims = len(sims)
#start simulating
tracker = pool.map_async(run_1_sim,sims)
while not tracker.ready():
    pass
    for i in range(num_sims):
        q = queues[i]
        try:
            gen = q.get(timeout = .001)
            # if the sim has updated, update the label
            #print gen
            svars[i].set(gen)
            self.update()
        except Empty:
            pass
# The results of the map, if necessary
tracker.get()

    def update(self):
        """
        Redraws everything
        """
        self.master.update_idletasks()

def run_1_sim(args):
    """
    Runs one simulation with the specified args, output updates to the supplied
    pipe every generation
    """
    name,config,data, verbose, q = args
    sim = Simulation(config, name=name, data = data)
    generation = 0
    q.put(sim.name + ": 0")
    try:
        while sim.run(verbose=verbose, log=True, generations = sim_step):
            generation += sim_step
            q.put(sim.name + ": " + str(generation))
    except Exception as err:
        print err
1个回答

2
这可能对你有所帮助,但需要注意的是,可以通过确保 tkinter 代码和方法在实例化根的特定线程上执行来使其支持多线程。在Python Cookbook上可以找到一个尝试这一概念的项目,作为recipe 577633(Directory Pruner 2)。下面的代码来自于第76-253行,并且很容易通过小部件进行扩展。

主要线程安全支持

# Import several GUI libraries.
import tkinter.ttk
import tkinter.filedialog
import tkinter.messagebox

# Import other needed modules.
import queue
import _thread
import operator

################################################################################

class AffinityLoop:

    "Restricts code execution to thread that instance was created on."

    __slots__ = '__action', '__thread'

    def __init__(self):
        "Initialize AffinityLoop with job queue and thread identity."
        self.__action = queue.Queue()
        self.__thread = _thread.get_ident()

    def run(self, func, *args, **keywords):
        "Run function on creating thread and return result."
        if _thread.get_ident() == self.__thread:
            self.__run_jobs()
            return func(*args, **keywords)
        else:
            job = self.__Job(func, args, keywords)
            self.__action.put_nowait(job)
            return job.result

    def __run_jobs(self):
        "Run all pending jobs currently in the job queue."
        while not self.__action.empty():
            job = self.__action.get_nowait()
            job.execute()

    ########################################################################

    class __Job:

        "Store information to run a job at a later time."

        __slots__ = ('__func', '__args', '__keywords',
                     '__error', '__mutex', '__value')

        def __init__(self, func, args, keywords):
            "Initialize the job's info and ready for execution."
            self.__func = func
            self.__args = args
            self.__keywords = keywords
            self.__error = False
            self.__mutex = _thread.allocate_lock()
            self.__mutex.acquire()

        def execute(self):
            "Run the job, store any error, and return to sender."
            try:
                self.__value = self.__func(*self.__args, **self.__keywords)
            except Exception as error:
                self.__error = True
                self.__value = error
            self.__mutex.release()

        @property
        def result(self):
            "Return execution result or raise an error."
            self.__mutex.acquire()
            if self.__error:
                raise self.__value
            return self.__value

################################################################################

class _ThreadSafe:

    "Create a thread-safe GUI class for safe cross-threaded calls."

    ROOT = tkinter.Tk

    def __init__(self, master=None, *args, **keywords):
        "Initialize a thread-safe wrapper around a GUI base class."
        if master is None:
            if self.BASE is not self.ROOT:
                raise ValueError('Widget must have a master!')
            self.__job = AffinityLoop() # Use Affinity() if it does not break.
            self.__schedule(self.__initialize, *args, **keywords)
        else:
            self.master = master
            self.__job = master.__job
            self.__schedule(self.__initialize, master, *args, **keywords)

    def __initialize(self, *args, **keywords):
        "Delegate instance creation to later time if necessary."
        self.__obj = self.BASE(*args, **keywords)

    ########################################################################

    # Provide a framework for delaying method execution when needed.

    def __schedule(self, *args, **keywords):
        "Schedule execution of a method till later if necessary."
        return self.__job.run(self.__run, *args, **keywords)

    @classmethod
    def __run(cls, func, *args, **keywords):
        "Execute the function after converting the arguments."
        args = tuple(cls.unwrap(i) for i in args)
        keywords = dict((k, cls.unwrap(v)) for k, v in keywords.items())
        return func(*args, **keywords)

    @staticmethod
    def unwrap(obj):
        "Unpack inner objects wrapped by _ThreadSafe instances."
        return obj.__obj if isinstance(obj, _ThreadSafe) else obj

    ########################################################################

    # Allow access to and manipulation of wrapped instance's settings.

    def __getitem__(self, key):
        "Get a configuration option from the underlying object."
        return self.__schedule(operator.getitem, self, key)

    def __setitem__(self, key, value):
        "Set a configuration option on the underlying object."
        return self.__schedule(operator.setitem, self, key, value)

    ########################################################################

    # Create attribute proxies for methods and allow their execution.

    def __getattr__(self, name):
        "Create a requested attribute and return cached result."
        attr = self.__Attr(self.__callback, (name,))
        setattr(self, name, attr)
        return attr

    def __callback(self, path, *args, **keywords):
        "Schedule execution of named method from attribute proxy."
        return self.__schedule(self.__method, path, *args, **keywords)

    def __method(self, path, *args, **keywords):
        "Extract a method and run it with the provided arguments."
        method = self.__obj
        for name in path:
            method = getattr(method, name)
        return method(*args, **keywords)

    ########################################################################

    class __Attr:

        "Save an attribute's name and wait for execution."

        __slots__ = '__callback', '__path'

        def __init__(self, callback, path):
            "Initialize proxy with callback and method path."
            self.__callback = callback
            self.__path = path

        def __call__(self, *args, **keywords):
            "Run a known method with the given arguments."
            return self.__callback(self.__path, *args, **keywords)

        def __getattr__(self, name):
            "Generate a proxy object for a sub-attribute."
            if name in {'__func__', '__name__'}:
                # Hack for the "tkinter.__init__.Misc._register" method.
                raise AttributeError('This is not a real method!')
            return self.__class__(self.__callback, self.__path + (name,))

################################################################################

# Provide thread-safe classes to be used from tkinter.

class Tk(_ThreadSafe): BASE = tkinter.Tk
class Frame(_ThreadSafe): BASE = tkinter.ttk.Frame
class Button(_ThreadSafe): BASE = tkinter.ttk.Button
class Entry(_ThreadSafe): BASE = tkinter.ttk.Entry
class Progressbar(_ThreadSafe): BASE = tkinter.ttk.Progressbar
class Treeview(_ThreadSafe): BASE = tkinter.ttk.Treeview
class Scrollbar(_ThreadSafe): BASE = tkinter.ttk.Scrollbar
class Sizegrip(_ThreadSafe): BASE = tkinter.ttk.Sizegrip
class Menu(_ThreadSafe): BASE = tkinter.Menu
class Directory(_ThreadSafe): BASE = tkinter.filedialog.Directory
class Message(_ThreadSafe): BASE = tkinter.messagebox.Message

如果您阅读应用程序的其余部分,就会发现它是使用已定义为_ThreadSafe变量的小部件构建的,这些小部件与其他tkinter应用程序中所见到的相同。当来自各个线程的方法调用到达时,它们会自动被保留,直到可以在创建线程上执行这些调用。请注意,通过291-298和326-336行进行了mainloop的替换。 注意NoDefaltRoot和main_loop调用
@classmethod
def main(cls):
    "Create an application containing a single TrimDirView widget."
    tkinter.NoDefaultRoot()
    root = cls.create_application_root()
    cls.attach_window_icon(root, ICON)
    view = cls.setup_class_instance(root)
    cls.main_loop(root)
允许线程执行。
@staticmethod
def main_loop(root):
    "Process all GUI events according to tkinter's settings."
    target = time.clock()
    while True:
        try:
            root.update()
        except tkinter.TclError:
            break
        target += tkinter._tkinter.getbusywaitinterval() / 1000
        time.sleep(max(target - time.clock(), 0))


1
我认为你是对的,创建自己的“主循环”是正确的方法。这样整个窗口都会更新,我可以拥有一些时髦的功能,比如“停止”按钮。 - Evlutte
在寻找使Tkinter循环协作的方法时遇到了这个问题,如果我正确地阅读了最后一段代码,我认为它不会按预期工作。.getbusywaitinterval()似乎返回一个整数(在我的解释器中=20),将其除以1000会得到整数0。因此,除非.getbusywaitinterval()返回超过1000的值,否则目标永远不会增加,我怀疑它通常不会这样做。 修复很容易,只需将1000更改为1000.0以执行浮点计算,这将最终使目标增加到高于0,并实际上让线程休眠。 - Ian E
我写了一个快速测试,发现原始代码确实总是执行time.sleep(0)。不幸的是,修复这个问题以执行适当的睡眠会导致睡眠不断增加,在几秒钟内达到1.5秒,这使应用程序变得非常缓慢。保留原始错误使得这个while循环运行非常快,并且占用相当多的处理器周期来等待输入。似乎没有简单的解决方法。 - Ian E
@IanE:Directory Pruner 4(http://code.activestate.com/recipes/578154/)使用了一种更好的跨GUI运行线程的方法。这是该程序最初引用的演变的一部分。 - Noctis Skytower
@noctis-skytower 这是一个更好的主循环:http://code.activestate.com/recipes/578153/ 它会在合理的时间内休眠(通常在我的系统上运行程序时为0.02秒),并且非常响应。不幸的是,对于我的特定应用程序和可能的其他应用程序,当用户按住按钮时,self.update() 不会返回(OSX/Py2.7)。它可能足够解决原始问题,但请注意,在用户操作界面时,UI 可能不会更新。 - Ian E

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