首页 > 解决方案 > 使用 Tkinter 时如何并行化方法

问题描述

如果其中一个使用 Tkinter 对象,我如何同时运行两种方法?

问题设置:方法 A 在给定时间过去后停止方法 B。在此之前,方法 A 在 Tkinter 标签中显示剩余时间。

问题:方法不会同时运行。

Python版本: 2.7

操作系统: Windows 7

我使用线程来实现并发。我读过 Python 有一个叫做 Global Interpreter Lock 的东西,它可以让线程串行运行。我认为这会导致问题。

一种解决方法是使用进程。这是不可能的,因为 Tkinter 对象无法转换为字符流(“pickle”)。当我尝试使用 Processes 时出现此错误: PicklingError: Can't pickle 'tkapp' object。

下面的可运行示例模仿了更大的真实程序。出于这个原因,它使用模型-视图-控制器设计模式。我在函数调用中从 Timeout复制了一些代码。

用例:用户单击一个按钮。这将启动一个可能需要很长时间的后台任务。在后台任务运行的同时,前端会不断的通知用户,距离程序取消后台任务还有多少时间。

后台运行的线程未实现,因此可以停止。但这不是我想知道的。

from Tkinter import *
from time import sleep
from threading import Thread, Timer

class Frontend(Tk):

    def __init__(self):

        Tk.__init__(self)

        self.label = Label(self, text = "", font = ("Courier", 12))
        self.button = Button(self, text = "Run thread in background.", font = ("Courier", 12))
        self.label.grid()
        self.button.grid(sticky = "nsew")

class Backend:

    def background_task(self):

        print "Background task is executing."
        sleep(6)
        print "Finished."

class Controller:

    def __init__(self):

        self.INTERRUPT_AFTER = 4
        self.done = True
        self.backend = Backend()
        self.frontend = Frontend()
        self.frontend.button.configure(command = self.run_something_in_background)

    class Decorator(object):

        def __init__(self, instance, time):

            self.instance = instance
            self.time = time

        def exit_after(self):
            def outer(fn):
                def inner():

                    timer = Timer(self.time, self.quit_function)
                    timer.start()
                    fn()

                    return timer
                return inner
            return outer

        def quit_function(self):

            if not self.instance.done:
                self.instance.display_message("Interrupted background task.")
                self.instance.set_done(True)

    def run_something_in_background(self):

        backendThread = Thread(target = self.backend.background_task)
        decorator = self.Decorator(self, self.INTERRUPT_AFTER)
        countdown = decorator.exit_after()(self.countdown)    # exit_after returns the function with which we decorate.

        self.set_done(False)
        countdown() 
        backendThread.start()
        backendThread.join()
        self.set_done(True)


    def countdown(self):

        seconds = self.INTERRUPT_AFTER
        while seconds > 0 and not self.done:
            message = "Interrupting background task in {} seconds\nif not finished.".format(str(seconds))
            self.display_message(message)
            seconds -= 1
            sleep(1)

    def set_done(self, val):

        self.done = val

    def display_message(self, message):

        self.frontend.label.config(text = message)
        self.frontend.update_idletasks()

    def run(self):

        self.frontend.mainloop()


app = Controller()
app.run()

标签: pythontkinterconcurrency

解决方案


尝试使用线程或多处理将面临的挑战是 tkinter 事件循环似乎受到后端线程/进程的影响。你能做的就是我在这里所做的。关键是使用 subprocess.Popen()。这会强制解释器打开另一个没有加载 tkinter 并且没有运行主循环的解释器(确保你没有)。

这是 frontend.py 程序:

from Tkinter import *
from subprocess import Popen

class Frontend(Tk):

    def __init__(self):

        Tk.__init__(self)
        self.label = Label(self, text = "", font = ("Courier", 12), justify='left')
        self.button = Button(self, text = "Run thread in background.", font = ("Courier", 12))
        self.label.grid()
        self.button.grid(sticky = "nsew")

class Controller:

    def __init__(self):

        self.INTERRUPT_AFTER = 4
        self.done = False
        self.frontend = Frontend()
        self.frontend.button.configure(command = self.run_something_in_background)

    def run_something_in_background(self, *args):

        self.set_done(False)
        seconds = 4
        self.frontend.after(int(seconds) * 1000, self.stopBackend)
        self.countdown(seconds) 

        self.backendProcess = Popen(['python', 'backend.py'])

    def stopBackend(self):
        self.backendProcess.terminate()
        self.done = True
        print 'Backend process terminated by frontend.'
        self.display_message('Backend process terminated')

    def countdown(self, remaining):
        print 'countdown', remaining
        if remaining > 0:
            message = "Interrupting background task in"
            message += " {} seconds\nif not finished.".format(str(remaining))
        elif self.done:
            message = 'Backend process completed'
        self.display_message(message)
        remaining -= 1
        if remaining > 0 and not self.done:
            self.frontend.after(1000, lambda s=remaining: self.countdown(s))
        else:
            message = 'Interrupting backend process.'
            self.display_message(message)

    def set_done(self, val):
        self.done = val

    def display_message(self, message):
        self.frontend.label.config(text = message)
        self.frontend.update_idletasks()

    def run(self):
        self.frontend.mainloop()

app = Controller()
app.run()

和 backend.py 代码:

from time import sleep


def background_task():

    print "Background task is executing."
    for i in range(8):
        sleep(1)
        print 'Background process completed', i+1, 'iteration(s)'
    print "Finished."

background_task()

推荐阅读