How do I run two methods concurrently if one of them is using Tkinter objects ?
Problem Setup: Method A stops method B when a given time has passed. Until then, method A displays the remaining time in a Tkinter label.
Problem: Methods do not run concurrently.
Python version: 2.7
OS: Windows 7
I use Threads to implement concurrency. I have read that Python has something called Global Interpreter Lock which makes threads run in serial. I assume that this causes the problem.
A workaround would be to use Processes. This is not possible since Tkinter objects can not be turned into character streams ("pickle"). I get this error when I try to use Processes: PicklingError: Can't pickle 'tkapp' object.
The runnable example below mimics the real program which is a lot larger. For this reason it uses the Model-View-Controller design pattern. I have copied some code from Timeout on a function call.
Use case: User clicks a button. This starts a background task which can take long time. While the background task is running, the front end continuously informs the user how much time is left until the program cancels the background task.
The thread running in background is not implemented so it can be stopped. But that's not what I'm wondering about anyway.
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()