Thread et sigterm

Document Actions

Par alexg le 18/06/2011 01:12

Si vous utilisez un processus qui utilise des thread et des lock, attention aux surprises sur la gestion de SIGTERM !

Catégories : autre
Modules python : signal,thread
Version Python : >2.5

Dans le monde POSIX, la commande kill envoie un signal (par défaut SIGTERM) au processus. Ce dernier doit avoir défini un traitement correspondant (handler), sans quoi il meurt.

En python si on veut pouvoir réagir au kill, on utilise le module signal, comme la documentation officielle en donne l'exemple

En voici un simpliste:

import signal, os, time

def handler(signum, frame):
  print 'Arrrgh !'
  exit(0)

print 'I am %d, ready to die !' % os.getpid()
signal.signal(signal.SIGTERM, handler)

while True:
    time.sleep(0.1)

Si je lance le programme, puis que je lance une commande kill dessus, il s'arrête, comme prévu:

$ python /tmp/tmp1.py &
[1] 16330
I am 16330, ready to die !
$ kill 16330
Arrrgh !
$  
[1] Done

Mais si à ça on mélange des thread:

import signal, os, time, threading

def leisure():
    while True:
        time.sleep(0.1)

t = threading.Thread(target=leisure)

def handler(signum, frame):
  print 'Arrrgh !'
  exit(0)

print 'I am %d, kill me if you can !' % os.getpid()
signal.signal(signal.SIGTERM, handler)


t.start()
# and me too
leisure()

Le handler est exécuté mais le programme ne s'arrête plus:

$ python /tmp/tmp2.py  &
[2] 16351
I am 16351, kill me if you can !
$ kill 16351
Arrrgh !
$ # remarquez que pas de Done !
$ ps
16351 python
$ kill -KILL 16351 # on ne peut resister à KILL
$ 
[2]+  Processus arrêté      python /tmp/tmp2.py

Ceci s'explique par le fait que le ``exit(0)`` de mon handler ne tue que le thread principal et pas le second.

Pour régler le problème j'ai deux solutions. La première est de déclarer que mon deuxième thread est peu important en lui mettant l'indicateur daemon, et dans ce cas lors de l'arrêt du thread principal, l'interpréteur arrêtera unilatéralement ce thread:

t = threading.Thread(target=leisure)
t.daemon = True
t.start()

La seconde possibilité est de gérer l'arrêt de mon thread moi-même grâce au handler (la solution sera spécifique). Par exemple:

import signal, os, time, threading

terminate = False

def leisure():
    while not terminate:
        time.sleep(0.1)

t = threading.Thread(target=leisure)

def handler(signum, frame):
  print 'Arrrgh !'
  global terminate
  terminate = True
  t.join()
  exit(0)

print 'I am %d, I know my dutty !' % os.getpid()
signal.signal(signal.SIGTERM, handler)


t.start()
# and me too
leisure()

Ça fonctionne:

$ python /tmp/tmp3.py  &
[1] 16631
I am 16631, I know my dutty !
alex@tignasse: ~$ kill 16631
Arrrgh !
alex@tignasse: ~$ 
[1]+  Done                    python /tmp/tmp3.py

Mais il y a encore un piège : la fonction de traitement du signal, le handler, ne peut s'exécuter qu'entre deux instructions de la machine virtuelle python.

Imaginez que votre thread principal attende un verrou:

import signal, os, time, threading

terminate = False
lock = threading.Lock()

def leisure():
    with lock:
        while not terminate:
            time.sleep(0.1)

t = threading.Thread(target=leisure)

def handler(signum, frame):
  print 'Arrrgh !'
  global terminate
  terminate = True
  t.join()
  exit(0)

print 'I am %d, too occupied to die !' % os.getpid()
signal.signal(signal.SIGTERM, handler)


t.start()
# and me too, a bit later to let t take the lock
time.sleep(0.1)
leisure()

Le handler n'est jamais appelé ! Il attend que le thread principal soit libre mais ça n'arrive jamais:

$ python /tmp/tmp4.py  &
[1] 16710
I am 16710, too occupied to die !
$ kill 16710
$ 
$ kill -KILL 16710
$ 
[1]+  Processus arrêté      python /tmp/tmp4.py

La solution ? Le mieux est que le thread principal ne s'occupe que d'attendre les signaux (et justement le module signal propose pause pour ça). Créez d'autre thread pour les tâches à faire:

import signal, os, time, threading

terminate = False
lock = threading.Lock()

def leisure():
    with lock:
        while not terminate:
            time.sleep(0.1)

t = threading.Thread(target=leisure)
t2 = threading.Thread(target=leisure)

def handler(signum, frame):
  print 'Arrrgh !'
  global terminate
  terminate = True
  t.join()
  t2.join()
  exit(0)

print "I am %d, I know I won't last !" % os.getpid()
signal.signal(signal.SIGTERM, handler)

t.start()
t2.start()
signal.pause()

Ça marche:

$ python /tmp/tmp5.py  &
[1] 16786
I am 16786, I know I won't last !
$ kill 16786
Arrrgh !
$ 
[1]+  Done                    python /tmp/tmp5.py

Python.org : Le site officiel du langage Python.
Zope.org : Le site web officiel de Zope.
Daily Python-URL : Actus de l'univers Python.
Tribute to Zyons : Zyons notre ami et membre fondateur de l'Afpy, nous quittait en 2005.