Reputation: 434
The only other question I can find about terminating Python timer threads is this one: how to terminate a timer thread in python but that only tells me what I already knew from reading the threading documentation.
I am writing a GTK app in Python. It is interfacing with HexChat via d-bus. Because there is no D-bus signal when a user changes context (switches to a different IRC channel or server window) my original design waited until the user interacted with the GUI in some way to see what context we are currently in. The downside to this is entry boxes which need to be switched out can't really be used to switch contexts, because if a user starts typing, the changed signal is issued and the contents get saved to the old context, which is incorrect behaviour.
To solve this, I decided to use a threaded timer task to check what the current context is every 0.5 seconds. The thread function checks the current context, updates a variable in the class, and then re-starts itself with another 0.5 second delay. This works perfectly and is fast enough that it will keep up with users switching channels often.
However, even when I add a TimerThread.cancel to my __del__()
function, instead of my program exiting it just hangs until I give a keyboard interrupt. I also tried doing a TimerThread.cancel, sleep(0.1), TimerThread.cancel again just in case I happened to cancel the timer as it was in the context-checking function. Same result. I also tried putting the TimerThread.cancel in the onDestroy function (which will in turn call the __del__
destructor) but no change. The GTK loop exits, and the GUI disappears, but the program just hangs in the console until keyboard interrupt. When I do give a keyboard interrupt, I get a traceback error from the threading library.
Is there some other way to terminate the thread? Is there some other step I need to take to kill the threading library before exiting? Am I misunderstanding how Timer threads work?
Here are the relevant sections of the code: https://paste.ubuntu.com/p/kQVfF78H5R/
EDIT: Here is the traceback, if that helps.
^CException ignored in: <module 'threading' from '/usr/lib/python3.7/threading.py'>
Traceback (most recent call last):
File "/usr/lib/python3.7/threading.py", line 1281, in _shutdown
t.join()
File "/usr/lib/python3.7/threading.py", line 1032, in join
self._wait_for_tstate_lock()
File "/usr/lib/python3.7/threading.py", line 1048, in _wait_for_tstate_lock
elif lock.acquire(block, timeout):
KeyboardInterrupt
Upvotes: 1
Views: 661
Reputation: 2320
I think you're running into a race condition. Try to protect the contextUpdater
variable with a lock. Here, I added three new functions, namely start_updater
which is called from the class __init__
method, stop_updater
, and restart_updater
which is called from the getContextTimer
method. Before exiting your application, call stop_updater
method which will cancel the currently running timer (don't call it from __del__
method):
def __init__(self):
self._lock = threading.Lock()
self.start_updater()
def start_updater(self):
with self._lock:
self._contextUpdater = threading.Timer(0.5, self.getContextTimer)
self._contextUpdater.start()
self._running = True
def stop_updater(self):
with self._lock:
if self._contextUpdater.is_alive():
self._contextUpdater.cancel()
self._running = False
def restart_updater(self):
with self._lock:
if not self._running:
return
self.start_updater()
def getContextTimer(self):
context = self.hcInterface.FindContext('', '')
try:
curChan = self.contextByID[int(context)]
except KeyError:
self.restart_updater()
return # we are in a context we don't care about
if curChan in self.inviteChannels:
self.getContext(None, 0)
elif curChan in self.supportChannels:
self.getContext(None, 1)
else:
self.getContext(None)
self.restart_updater()
I tested this approach with the following toy example:
class FooTimer(object):
def __init__(self):
self._lock = Lock()
self.start()
def start(self):
with self._lock:
self._timer = Timer(1, self.update)
self._timer.start()
self._running = True
def stop(self):
with self._lock:
if self._timer.is_alive():
self._timer.cancel()
self._running = False
def restart(self):
with self._lock:
if not self._running:
return
self.start()
def update(self):
print('tick')
self.restart()
Upvotes: 1