1

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()
gefdel
  • 59
  • 1
  • 7
  • Have you tried multiprocessing? – Mike - SMT May 24 '18 at 14:17
  • Yes I have. They are not possible to use with Tkinter since Tkinter objects are not possible to transform into character streams ("pickle"). I get an error when trying, saying PicklingError: Can't pickle 'tkapp' object. – gefdel May 24 '18 at 17:13
  • Well I know multiprocessing can work with tkinter. There are examples here on stack overflow. However I know nothing about "pickling" as I have only heard about it and never attempted to use it so I can't really speak on that portion. – Mike - SMT May 24 '18 at 17:15

1 Answers1

2

The challenge you will have trying to use either threading or multiprocessing is that the tkinter event loop seems to be impacted by the backend thread/process. What you can do is what I've done here. The key is using subprocess.Popen(). This forces the interpreter to open another interpreter that has not loaded tkinter and is not running a mainloop (make sure you don't).

This is the frontend.py program:

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

And the backend.py code:

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()
Ron Norris
  • 2,642
  • 1
  • 9
  • 13