Andyroo

[SOLVED] Handling Ctrl-C / Kill in threads

Mon Mar 18, 2019 4:06 pm

I have a problem on handling abrupt terminations in Python 3 :oops: :cry:

I have a function set by

Code: Select all

signal.signal(signal.SIGINT, signal_handler)
that basically just logs that the pgm was killed and exits:

Code: Select all

def signal_handler(signal, frame):
    log('Program terminated by kill or ctrl-c', dbgmsg = False)
    sys.exit(0)
This was fine till I added a background task kicked off into its own thread - this runs a CPU / GPU temp check, reports it by MQTT and waits in a time.sleep but now when I kill the program I cannot exit cleanly (often taking multiple ctrl-c) without errors such as:
Exception ignored in: <module 'threading' from '/usr/lib/python3.5/threading.py'>
Traceback (most recent call last):
File "/usr/lib/python3.5/threading.py", line 1288, in _shutdown
t.join()
File "/usr/lib/python3.5/threading.py", line 1054, in join
self._wait_for_tstate_lock()
File "/usr/lib/python3.5/threading.py", line 1070, in _wait_for_tstate_lock
elif lock.acquire(block, timeout):
File "BaseClient.py", line 27, in signal_handler
Anyone got any ideas for handling kills in threads especially any that you are in a sleep wait?
I can change the sleep state to a second and loop for 300 of them :shock: but I'm not sure that would help.

Next thing is to try setting the handler again in the thread routine...
Last edited by Andyroo on Wed Mar 27, 2019 3:35 pm, edited 1 time in total.

Andyroo

Re: Handling Ctrl-C / Kill in threads

Mon Mar 18, 2019 4:16 pm

Well setting the handler in the thread just gave me:
Exception in thread Thread-1:
Traceback (most recent call last):
File "/usr/lib/python3.5/threading.py", line 914, in _bootstrap_inner
self.run()
File "/usr/lib/python3.5/threading.py", line 862, in run
self._target(*self._args, **self._kwargs)
File "BaseClient.py", line 203, in pushchecktemp
signal.signal(signal.SIGINT, signal_handler)
File "/usr/lib/python3.5/signal.py", line 47, in signal
handler = _signal.signal(_enum_to_int(signalnum), _enum_to_int(handler))
ValueError: signal only works in main thread
:o :lol:

Andyroo

Re: Handling Ctrl-C / Kill in threads

Mon Mar 18, 2019 4:33 pm

I've now found https://www.g-loaded.eu/2016/11/24/how- ... g-signals/ that seems to have a solution :oops:

Time to have a play and I'll update this with a solved and code sample when it works :lol:

User avatar
jahboater
Posts: 6134
Joined: Wed Feb 04, 2015 6:38 pm
Location: West Dorset

Re: Handling Ctrl-C / Kill in threads

Mon Mar 18, 2019 5:20 pm

I don't know if this will help, or if this system call is available in Python (probably is)

Code: Select all

NAME
       exit_group - exit all threads in a process

SYNOPSIS
       #include <linux/unistd.h>

       void exit_group(int status);

DESCRIPTION
       This system call is equivalent to exit(2) except that it terminates not only the calling
       thread, but all threads in the calling process's thread group.

RETURN VALUE
       This system call does not return.

VERSIONS
       This call is present since Linux 2.5.35.

User avatar
paddyg
Posts: 2566
Joined: Sat Jan 28, 2012 11:57 am
Location: UK

Re: Handling Ctrl-C / Kill in threads

Tue Mar 19, 2019 8:43 am

Not sure if it's applicable wrt signal stuff but I usually either set daemon = True or try catch KeyboardInterrupt depending on what tidying up needs doing.

Code: Select all

import time
import threading
def f1():
  while True:
    time.sleep(1.0)
  print('stopping because of daemon kill') # will never get here

def f2(a):
  while a[0]:
    time.sleep(1.0)
  print('message sent after kb intrpt') # can do some graceful shutting down
'''
#daemon True version
t = threading.Thread(target=f1, daemon=True)
t.start()
while True:
  time.sleep(0.5)
'''
#kbintrpt version
a = [True] # can pass other stuff for graceful shutdown here
t = threading.Thread(target=f2, args=(a,))
t.start()
try:
  while True:
    time.sleep(0.5)
except KeyboardInterrupt:
  a[0] = False # and can do other tidying here
#'''
also https://groups.google.com/forum/?hl=en-GB&fromgroups=#!forum/pi3d

Andyroo

Re: Handling Ctrl-C / Kill in threads

Tue Mar 19, 2019 1:01 pm

Thanks for the ideas folks.

Having spent a fair bit of time reading last night it looks like the long sleep time (10 minutes in one case) is the main issue and I may do better to break it down into second intervals with boolean flag check as an exit condition.

If I can stay awake tonight I'll have a play and post the results.

Looking at the processor use when running the base program and two wait task threads in Python I only see one entry under ps:
pi 5370 13.6 4.4 61600 21968 pts/0 Sl+ 12:57 0:03 python3 BaseClient.py
so I assume the threading is handled by Python itself - very neat esp on a one core Zero W :lol:

Andyroo

[SOLVED] Handling Ctrl-C / Kill in threads

Wed Mar 27, 2019 3:35 pm

That was a longer sleep than I thought :lol:

I need a lot more Python experience before I understand the class and signals used here so I ended up with creating a global boolean (RunThread) monitored by all threads and coding:

A link to the kill handler for the three signals to trap IN THE MAIN CODE:

Code: Select all

for sig in ('TERM', 'HUP', 'INT'):
    signal.signal(getattr(signal, 'SIG' + sig), KillHandler);
This routine now looks like this:

Code: Select all

def KillHandler(signal, frame):
    global RunThread
    RunThread = False
    ThreadHeart.join()
    ThreadTemp.join()
    sys.exit(0)
Note:
1) The RunThread variable is used in both threads to flag if they should end or not
2) The join() is used to let the threads end
3) Time is in seconds so I use the math to remind me that it 'x' seconds with a 0.5 second delay so for 3 minutes delay is 3 * 60 * 2 rather than a count of 360

A sample thread now looks like:

Code: Select all

def pushheartbeat():
    global RunThread
    global Computer
    cmd = {
        "Device" : Computer ,
        "Command" : 'Heartbeat'
    }
    while RunThread:
        delay = 3 * 60 * 2
        while (delay > 0) and RunThread:
            time.sleep(0.5)
            delay -= 1
            cmdq.put(cmd)
So if the kill handler sets the RunThread to False this routine will end else it will go on forever.
I did decide to build in a 'quit' option (MQTT message) so the flag and join() routines are duplicated at the end of the program if it ends normally :oops:

Long term improvements:
1) Record all thread objects into a list so I do not have to add a join command for each one - I could end up with 8 / 10 threads in all
2) Merge the normal and abort endings into one routine
3) Change -= back to - 1 (much easier to read in my eyes) :lol:

Thanks to the folk who gave me ideas on this thread and showed me a bit more Python.

Return to “Python”