How to avoid memory leak when using Threading.Timer

Viewed 251

I have a requirement for a timer based event handler that runs every X seconds, but can be stopped and started at will. I have this following code I found at Stack Overflow, which is absolutely perfect for my needs, however a change in Python at version 3.7.4 introduced a leak for any thread that doesn't get it's "join" method called.

from threading import Timer


class InfiniteTimer():
    """A Timer class that does not stop, unless you want it to."""

    def __init__(self, seconds, target):
        self._should_continue = False
        self.is_running = False
        self.seconds = seconds
        self.target = target
        self.thread = None

    def _handle_target(self):
        self.is_running = True
        self.target()
        self.is_running = False
        self._start_timer()

    def _start_timer(self):
        if self._should_continue: # Code could have been running when cancel was called.
            self.thread = Timer(self.seconds, self._handle_target)
            self.thread.start()

    def start(self):
        if not self._should_continue and not self.is_running:
            self._should_continue = True
            self._start_timer()
        else:
            print("Timer already started or running, please wait if you're restarting.")

    def cancel(self):
        if self.thread is not None:
            self._should_continue = False # Just in case thread is running and cancel fails.
            self.thread.cancel()
        else:
            print("Timer never started or failed to initialize.")


def tick():
    print('ipsem lorem')

# Example Usage
t = InfiniteTimer(0.5, tick)
t.start()

When the code above runs, every half a second, a new _thread.lock is created, and if I cancel and re-start, again a new _thread.lock is created.

I've done some research and found some information at https://bugs.python.org/issue43050 and https://bugs.python.org/issue37788 where it suggests that you need to "join" the thread for it to be able to successfully free up the object after it is used. One of the solutions mentioned suggests joining after thread.cancel() is called, but when I do this I get an exception

RuntimeError("cannot join current thread")

I have also tried joining after the self.thread.start() and self._start_timer() both of which causes exceptions too.

What can I do to the above code to completely obliterate all the thread_lock() memory leaks it creates?

1 Answers

Essentially, what I am trying to do is not really possible without creating an infinite number of _thread.lock objects. The current Python timer is just not geared up in such a way that it can provide the functionality I need. According to the documentation at https://docs.python.org/3/library/threading.html, the class represents an action that should be run only after a certain amount of time has passed — a timer.

Timers are started, as with threads, by calling their start() method. The timer can be stopped (before its action has begun) by calling the cancel() method. The interval the timer will wait before executing its action may not be exactly the same as the interval specified by the user.

This means that timers in Python are nothing like timers in other languages like C#, where they can be stopped and started at will, and can repeat and even have their execution time changed on the fly.

The biggest problem I'm facing is that whenever I re-start the timer (so that it can repeat), I'm creating a new timer from within an existing timer, which means the new thread is a child of the existing thread - so the garbage collector cannot free up the object as it has a reference to the child thread. That child thread gets a child thread so we end up with grandchildren and great-grandchildren and so forth, a never-ending chain of threads which is your memory leak.

I've resolved my problem by implementing a thread class that contains the functionality that I needed, and here it is:

class TimerClass(threading.Thread):

    def __init__(self, callback, sleep):
        super().__init__()
        self.callback = callback
        self.sleep = sleep
        self.running = True
        self.active = False
        
    def run(self):
        while self.running: # Loop while self.running is true
            if self.active:
                for x in range(0,self.sleep):  # loop self.sleep seconds, before checking if active again
                    if self.active: # Continue checking if we're active, in which case, continue delaying
                        time.sleep(1)
                self.callback() # If the Timer is activated, then run our function
            else:
                time.sleep(float(0.1))
            
    def stop(self):
        self.active = False
        self.running = False

    def activate(self):
        self.active = True
    
    def deactivate(self):
        self.active = False
    
    def setactive(self, active):
        self.active = active

Creating the timer is done once, and it will start immediately after being initialized, like this:

def myfunction():
    print("Hello World!")

mytimer = TimerClass(myfunction, 60)
mytimer.start()

pausing the timer is done like so:

mytimer.setactive(False)

or:

mytimer.deactivate()

Resuming the timer is done like so:

mytimer.setactive(True)

or:

mytimer.activate()

Stopping the timer and allowing it to be disposed:

mytimer.stop()
mytimer.join()

Determining if the timer is active or not can be done by reading the variable mytimer.active

Hope this is helpful for others who have similar needs.

Related