threading.Thread mit signal.signal unterbrechen, wie?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
zybork
User
Beiträge: 3
Registriert: Freitag 4. Mai 2012, 15:05

Hallo Leute!

Folgendes quälendes Problem: Ich will einen Thread mit einem Signal (wir reden von Linux hier) unterbrechen, wieso funktioniert folgender Code nicht?

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: utf-8 -*-

import threading
import time
import os
import sys
import signal

print os.getpid()

time.sleep(2)

Running = True

class Process(threading.Thread):
  def __init__(self):
    self.running = True
    
    threading.Thread.__init__(self)

  def run(self):
    for iter in range(10):
      print iter
      if not Running:
        print "I break now"
        break
      time.sleep(1)


def sighup(signal, frame):
  Running = False
  print "  ** sighup **"

signal.signal(signal.SIGHUP, sighup)

my = Process()
my.start()

for iter in ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"]:
  print iter
  time.sleep(1.4)
Beim Ausführen sieht man, dass der Signal-Handler aufgerufen wird, aber offenbar funzt das mit der globalen Variablen nicht. Wie macht man sowas? Ich habe nach eineinhalb Stunden im Guugl bzw. Herumexperimentieren nicht herausgefunden, wie es geht.
Zuletzt geändert von Anonymous am Samstag 5. Mai 2012, 09:12, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
Benutzeravatar
pillmuncher
User
Beiträge: 1532
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

Hallo & Willkommen im Forum.

Üblich sind 4 spaces pro Einrückungsebene, nicht zwei. Mehr dazu in PEP8.

iter ist der Name einer eingebauten Funktion. Wenn du eine Variable so benennst, verdeckst du diese eingebaute Funktion in deinem scope.

Globale Variablen funktionieren in Python anders als in den meisten anderen Sprachen. Erstens sind sie nur global in Bezug auf das Modul, in dem sie definiert sind, zweitens kann man sie in Funktionen zwar lesen, aber sobald man ein Zuweisung an sie im Code stehen hat, interpretiert Python das als Definition einer lokalen Variablen, die die globale verdeckt. Das ist kein Bug, sondern ein Feature. Um dem zu entgehen muss man die globale Variable im scope der Funktion als global deklarieren:

Code: Alles auswählen

my_var = 'hallo'

def my_func():
    global my_var
    my_var = 'jetzt neu!'

my_func()

assert my_var == 'jetzt neu!'
Weil das alles so clumsy ist, meiden erfahrene Pythonistas globale Variablen wie der sprichwörtliche Teufel das Weihwasser. Statt dessen würden wir eher sowas empfehlen:

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: utf-8 -*-

import threading
import time
import os
import signal

print os.getpid()

time.sleep(2)


class Process(threading.Thread):

    def run(self):
        self.cancelled = False
        for each in range(10):
            print each
            if self.cancelled:
                print "I break now"
                break
            time.sleep(1)

    def sighup(self, signal, frame):
        self.cancelled = True
        print "  ** sighup **"

my = Process()
signal.signal(signal.SIGHUP, my.sighup)
my.start()

for word in ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"]:
    print word
    time.sleep(1.4)
Zuletzt geändert von pillmuncher am Freitag 4. Mai 2012, 17:31, insgesamt 1-mal geändert.
In specifications, Murphy's Law supersedes Ohm's.
anogayales
User
Beiträge: 456
Registriert: Mittwoch 15. April 2009, 14:11

So wie es jetzt aussieht, willst du den Thread beenden bevor er überhaupt ausgeführt wird. Sowas kann nicht funktionieren.
Benutzeravatar
pillmuncher
User
Beiträge: 1532
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@anogayales: Nein, das stimmt schon so. signal.signal sendet kein signal, sondern istalliert einen signal handler.
In specifications, Murphy's Law supersedes Ohm's.
anogayales
User
Beiträge: 456
Registriert: Mittwoch 15. April 2009, 14:11

Und wo wird dieses Signal ausgelöst? Im ursprünglichen Quelltext seh ich nämlich davon nix :) Vielleicht lern ich ja was dazu :P

Grüße,
anogayales
BlackJack

@anogayales: Das Signal kommt von aussen, vom Betriebssystem. Zum Beispiel wenn das Programm über eine Remoteverbindung gestartet wurde und die Gegenseite die Verbindung schliesst. HUP ist die Abkürzung für HangUP. Kommt noch aus Zeiten wo man Modems über die Telefonleitung benutzte und das Modem quasi den „Hörer” aufgelegt hat.
Benutzeravatar
pillmuncher
User
Beiträge: 1532
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@anogayales: Was BlackJack gesagt hat. Du kannst auch, wenn du schnell bist, sowas machen: Zwei Shell-Fenster nebeneinander öffnen. Im ersten:

Code: Alles auswählen

mick@haddock ~
$ python threadsignal.py
5232
Ich habe die bash verwendet. Die Zahl ist die process id. Damit kannst du im zweiten Fenster dieses tun:

Code: Alles auswählen

mick@haddock ~
$ kill -HUP 5232
Die weitere Ausgabe im ersten Fenster sieht dann ungefähr so aus:

Code: Alles auswählen

alpha
0
1
beta
2
gamma
3
4
delta
5
  ** sighup **
epsilon
6
I break now
zeta
eta
theta
In specifications, Murphy's Law supersedes Ohm's.
anogayales
User
Beiträge: 456
Registriert: Mittwoch 15. April 2009, 14:11

Vielen herzlichen Dank! Wieder was dazu gelernt!

Grüße,
anogayales
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Wobei ich mir nicht sicher bin, ob das Signalhandling dergestalt (über Methode des Subthreads) wirklich "threadsafe" ist oder nicht zu Nebeneffekten führen kann. Zumindest sagt die Doku zu signals, das nur der Hauptthread die signals erhält. Funktioniert Dein Bsp. denn in dieser Form?

Alternativ müsste man den Hauptthread mit einem entsprechenden Handler austatten und den Subthread dann per Event beenden.
Benutzeravatar
pillmuncher
User
Beiträge: 1532
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@jerch: Ich hab es mal folgendermaßen angepasst:

Code: Alles auswählen

...
class Process(threading.Thread):

    def run(self):
        print 'sub: %s' % threading.current_thread()
        self.cancelled = False
        for each in range(10):
            print each
            if self.cancelled:
                print "I break now"
                break
            time.sleep(1)

    def sighup(self, signal, frame):
        print 'sighup: %s' % threading.current_thread()
        self.cancelled = True
        print "  ** sighup **"

print 'main: %s' % threading.current_thread()
my = Process()
signal.signal(signal.SIGHUP, my.sighup)
my.start()
...
Ergebnis:

Code: Alles auswählen

$ python threadsignal.py
6032
main: <_MainThread(MainThread, started 536870968)>
sub: <Process(Thread-1, started 536945680)>alpha

0
sighup: <_MainThread(MainThread, started 536870968)>
  ** sighup **
beta
1
I break now
gamma
delta
epsilon
zeta
eta
theta
Der signal handler läuft also im Hauptthread. Die handler Methode ist zwar eine Methode des Process-Objekts (also einer Instanz einer von threading.Thread abgeleiteten Klasse), aber dieses ist ja nicht selbst der Thread, sondern repräsentiert ihn nur. Auf dieser Repräsentation kann man von von jedem beliebigen Thread aus jede Methode aufrufen, und die aufgerufene Methode läuft dann im aufrufenden Thread. Laut Doku garantiert Python, dass Signale immer im Hauptthread landen, weswegen auch nur dort ein signal handler installiert werden darf. Ob durch den Aufruf des signal handlers eine race condition entsteht, lässt sich damit genauso einfach/schwierig feststellen, wie in dem Fall, wo man die signal handler Funktion selbst aus dem Hautthread heraus aufruft. Im vorliegenden Fall könnte es passieren, dass der signal handler aufgerufen wird, bevor die run-Methode gestartet wurde. Dabei würde cancelled zuerst auf True und danach auf False gesetzt, es wäre also so, als ob das Signal nie ausgelöst worden wäre. Deswegen würde ich im vorliegenden Fall die Aufrufreihenfolge im Hauptthread ändern und ein Latch verwenden:

Code: Alles auswählen

...
class Latch(object):

    def __init__(self):
        self._cond = threading.Condition(threading.Lock())
        self._flag = False

    def set(self):
        with self._cond:
            if self._flag:
                raise RuntimeError('Latch has already been set!')
            self._flag = True
            self._cond.notifyAll()

    def await(self):
        with self._cond:
            while not self._flag:
                self._cond.wait()


class Process(threading.Thread):

    def __init__(self, latch):
        self.latch = latch
        threading.Thread.__init__(self)

    def run(self):
        self.cancelled = False
        self.latch.set()  # <== hier
        for each in range(10):
            print each
            if self.cancelled:
                print "I break now"
                break
            time.sleep(1)

    def sighup(self, signal, frame):
        self.cancelled = True
        print "  ** sighup **"

latch = Latch()
my = Process(latch)
my.start()
latch.await()  # <== und hier
signal.signal(signal.SIGHUP, my.sighup)
...
Damit ist garantiert, dass der signal handler frühestens aufgerufen werden kann, nachdem cancelled mit False initialisiert wurde.
In specifications, Murphy's Law supersedes Ohm's.
zybork
User
Beiträge: 3
Registriert: Freitag 4. Mai 2012, 15:05

Pillmuncher:

> Üblich sind 4 spaces pro Einrückungsebene, nicht zwei. Mehr dazu in PEP8.

Darüber könnten wir jetzt einen Religionskrieg führen ;) Zwei Leerzeichen (der „Double-Tap“ der Leertaste) haben in mancher Hinsicht gravierende Vorteile, aber wie gesagt, dazu vielleicht einmal ein eigener Thread.

Ich habe übrigens auch schon lokale Variablen verwendet gehabt, ohne, dass es funktioniert hätte, weshalb ich überhaupt auf die Idee mit der globalen Variablen verfallen bin.

Wenn ich das jetzt allerdings wieder so mach, wie vorher, sprich, self.running=True in __init__ definieren, funktioniert es jetzt, und zwar ganz egal, ob ich diese Zuweisung im __init__ der von threading.Thread abgeleiteten Klasse vor threading.Thread.__init__(self) setze oder nachher. Ich habe also entweder einen der merkwürdigsten Bugs aller Zeiten auf dem Hals gehabt, oder aber in all den Versuchen vorher irgendeinen kleinen, unscheinbaren, aber ausschlaggebenden Fehler gemacht.

Das Schlüsselwort global ist mir bekannt, ich glaube, ich habe schlicht im Laufe der Zeit keine Konzentrationsfähigkeit mehr gehabt, wie auch immer, dass iter ein Builtin ist, war mir nicht bekannt, danke für die Information.

...

Jetzt funktioniert es auch, wenn ich signal.signal mit den entsprechenden Parametern aus __init__ heraus aufrufe. Ich frage mich wirklich, wo (bevor ich die globale Variable genommen habe) mein Fehler gelegen ist. Sei's drum, danke für die Hilfe.

PS: Offensichtlich ist es nicht üblich, hier gelöste Probleme mit „SOLVED“ zu markieren.
BlackJack

@zybork: Wenn Du bei einer Sprache wo Einrückung zur Semantik gehört und man nur unkompliziert zusammen arbeiten kann, wenn dabei alle Beteiligten der gleichen Konvention folgen, einen Religionskrieg führen möchtest, dann bring wenigstens bessere Argumente als das man durch mehrfaches drücken der Leertaste bei zwei Leerzeichen weniger Arbeit hat. Ich kenne echt *keinen* Programmierer der mit der Leertaste einrückt. Das macht entweder der Editor an den Stellen wo er das erkennen kann von alleine oder man benutzt zum Einrücken einer Ebene *einen* Druck auf die Tabulatortaste. Und zum Ausrücken einer Ebene bei den meisten Editoren die Umschalttaste + Tabulatortaste.
zybork
User
Beiträge: 3
Registriert: Freitag 4. Mai 2012, 15:05

aber wie gesagt, dazu vielleicht einmal ein eigener Thread.
Okay, diskutier ich gern aus, aber – du bist eh ein Moder., du kannst wohl einen neuen Thread draus machen – ein eigener Thread wär hier wohl angebracht, weil keine Relevanz zum ursprünglichen Thema.
Antworten