Pregunta

¿Es posible terminar un subproceso en ejecución sin configurar / verificar ningún indicador / semáforo / etc.?

¿Fue útil?

Solución

Generalmente es un mal patrón matar un hilo abruptamente, en Python y en cualquier idioma. Piense en los siguientes casos:

  • el hilo contiene un recurso crítico que debe cerrarse correctamente
  • el hilo ha creado varios otros hilos que también deben ser eliminados.

La mejor manera de manejar esto si puede permitírselo (si está administrando sus propios subprocesos) es tener un indicador de salida_request que cada subproceso verifique en el intervalo regular para ver si es hora de que salga.

Por ejemplo:

import threading

class StoppableThread(threading.Thread):
    """Thread class with a stop() method. The thread itself has to check
    regularly for the stopped() condition."""

    def __init__(self):
        super(StoppableThread, self).__init__()
        self._stop_event = threading.Event()

    def stop(self):
        self._stop_event.set()

    def stopped(self):
        return self._stop_event.is_set()

En este código, debe llamar a stop () en el hilo cuando quiera que salga y esperar a que el hilo salga correctamente usando join () . El hilo debe verificar la bandera de detención a intervalos regulares.

Sin embargo, hay casos en los que realmente necesitas matar un hilo. Un ejemplo es cuando está ajustando una biblioteca externa que está ocupada por llamadas largas y desea interrumpirla.

El siguiente código permite (con algunas restricciones) generar una excepción en un hilo de Python:

def _async_raise(tid, exctype):
    '''Raises an exception in the threads with id tid'''
    if not inspect.isclass(exctype):
        raise TypeError("Only types can be raised (not instances)")
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid),
                                                     ctypes.py_object(exctype))
    if res == 0:
        raise ValueError("invalid thread id")
    elif res != 1:
        # "if it returns a number greater than one, you're in trouble,
        # and you should call it again with exc=NULL to revert the effect"
        ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), None)
        raise SystemError("PyThreadState_SetAsyncExc failed")

class ThreadWithExc(threading.Thread):
    '''A thread class that supports raising exception in the thread from
       another thread.
    '''
    def _get_my_tid(self):
        """determines this (self's) thread id

        CAREFUL : this function is executed in the context of the caller
        thread, to get the identity of the thread represented by this
        instance.
        """
        if not self.isAlive():
            raise threading.ThreadError("the thread is not active")

        # do we have it cached?
        if hasattr(self, "_thread_id"):
            return self._thread_id

        # no, look for it in the _active dict
        for tid, tobj in threading._active.items():
            if tobj is self:
                self._thread_id = tid
                return tid

        # TODO: in python 2.6, there's a simpler way to do : self.ident

        raise AssertionError("could not determine the thread's id")

    def raiseExc(self, exctype):
        """Raises the given exception type in the context of this thread.

        If the thread is busy in a system call (time.sleep(),
        socket.accept(), ...), the exception is simply ignored.

        If you are sure that your exception should terminate the thread,
        one way to ensure that it works is:

            t = ThreadWithExc( ... )
            ...
            t.raiseExc( SomeException )
            while t.isAlive():
                time.sleep( 0.1 )
                t.raiseExc( SomeException )

        If the exception is to be caught by the thread, you need a way to
        check that your thread has caught it.

        CAREFUL : this function is executed in the context of the
        caller thread, to raise an excpetion in the context of the
        thread represented by this instance.
        """
        _async_raise( self._get_my_tid(), exctype )

(Basado en Subprocesos eliminables de Tomer Filiba. La cita sobre el valor de retorno de PyThreadState_SetAsyncExc parece ser de un versión anterior de Python .)

Como se señala en la documentación, esto no es una bala mágica porque si el hilo está ocupado fuera del intérprete de Python, no detectará la interrupción.

Un buen patrón de uso de este código es hacer que el hilo detecte una excepción específica y realice la limpieza. De esa manera, puede interrumpir una tarea y aún tener la limpieza adecuada.

Otros consejos

No hay una API oficial para hacer eso, no.

Debe usar la API de plataforma para eliminar el hilo, p. ej. pthread_kill o TerminateThread. Puede acceder a dicha API, p. a través de pythonwin, o a través de ctypes.

Tenga en cuenta que esto es inherentemente inseguro. Probablemente conducirá a basura incobrable (de variables locales de los marcos de la pila que se convierten en basura), y puede conducir a puntos muertos, si el hilo que se está matando tiene el GIL en el momento en que se mata.

Un multiprocessing.Process puede p.terminate()

En los casos en que quiero matar un hilo, pero no quiero usar banderas / bloqueos / señales / semáforos / eventos / lo que sea, promuevo los hilos a procesos completos. Para el código que utiliza solo unos pocos hilos, la sobrecarga no es tan mala.

Por ejemplo. esto es útil para terminar fácilmente los hilos auxiliares '' que ejecutan el bloqueo de E / S

La conversión es trivial: en el código relacionado, reemplace todos los threading.Thread con multiprocessing.Process y todos los queue.Queue con multiprocessing.Queue y agregue las llamadas requeridas de p.terminate () a su proceso padre que quiere matar a su hijo p

Documento de Python

Si está intentando terminar todo el programa, puede configurar el hilo como un "daemon". ver Thread.daemon

Esto se basa en thread2 - hilos que se pueden matar (receta de Python)

Debe llamar a PyThreadState_SetasyncExc (), que solo está disponible a través de ctypes.

Esto solo se ha probado en Python 2.7.3, pero es probable que funcione con otras versiones 2.x recientes.

import ctypes

def terminate_thread(thread):
    """Terminates a python thread from another thread.

    :param thread: a threading.Thread instance
    """
    if not thread.isAlive():
        return

    exc = ctypes.py_object(SystemExit)
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
        ctypes.c_long(thread.ident), exc)
    if res == 0:
        raise ValueError("nonexistent thread id")
    elif res > 1:
        # """if it returns a number greater than one, you're in trouble,
        # and you should call it again with exc=NULL to revert the effect"""
        ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None)
        raise SystemError("PyThreadState_SetAsyncExc failed")

Nunca debes matar a la fuerza un hilo sin cooperar con él.

Eliminar un hilo elimina cualquier garantía que intente / finalmente bloquee la configuración, por lo que puede dejar bloqueados, archivos abiertos, etc.

La única vez que puede argumentar que matar hilos a la fuerza es una buena idea es matar un programa rápidamente, pero nunca hilos individuales.

Como otros han mencionado, la norma es establecer una bandera de detención. Para algo ligero (sin subclases de Thread, sin variable global), una devolución de llamada lambda es una opción. (Tenga en cuenta los paréntesis en if stop () .)

import threading
import time

def do_work(id, stop):
    print("I am thread", id)
    while True:
        print("I am thread {} doing something".format(id))
        if stop():
            print("  Exiting loop.")
            break
    print("Thread {}, signing off".format(id))


def main():
    stop_threads = False
    workers = []
    for id in range(0,3):
        tmp = threading.Thread(target=do_work, args=(id, lambda: stop_threads))
        workers.append(tmp)
        tmp.start()
    time.sleep(3)
    print('main: done sleeping; time to stop the threads.')
    stop_threads = True
    for worker in workers:
        worker.join()
    print('Finis.')

if __name__ == '__main__':
    main()

Reemplazar print () con una función pr () que siempre enjuaga ( sys.stdout.flush () ) puede mejorar la precisión de la salida del shell.

(Solo probado en Windows / Eclipse / Python3.3)

En Python, simplemente no puedes matar un hilo directamente.

Si realmente NO necesita tener un subproceso (!), qué puede hacer, en lugar de utilizar el subprocesamiento paquete , es utilizar el paquete de multiprocesamiento . Aquí, para matar un proceso, simplemente puede llamar al método:

yourProcess.terminate()  # kill the process!

Python matará su proceso (en Unix a través de la señal SIGTERM, mientras que en Windows a través de la llamada TerminateProcess () ). ¡Presta atención para usarlo mientras usas una cola o una tubería! (puede dañar los datos en la Cola / Canalización)

Tenga en cuenta que el multiprocessing.Event y el multiprocessing.Semaphore funcionan exactamente de la misma manera que el threading.Event y el threading.Semaphore respectivamente. De hecho, los primeros son clones de los últimos.

Si REALMENTE necesitas usar un hilo, no hay forma de matarlo directamente. Sin embargo, lo que puede hacer es utilizar un " hilo de demonio " . De hecho, en Python, un Thread se puede marcar como daemon :

yourThread.daemon = True  # set the Thread as a "daemon thread"

El programa principal se cerrará cuando no queden hilos no daemon vivos. En otras palabras, cuando su hilo principal (que es, por supuesto, un hilo que no sea de demonio) termine sus operaciones, el programa se cerrará incluso si todavía hay algunos hilos de demonio funcionando.

Tenga en cuenta que es necesario establecer un subproceso como daemon antes de que se llame al método start () !

Por supuesto que puede y debe usar daemon incluso con multiprocesamiento . Aquí, cuando finaliza el proceso principal, intenta finalizar todos sus procesos secundarios demoníacos.

Finalmente, tenga en cuenta que sys.exit () y os.kill () no son opciones.

Puede matar un hilo instalando el rastreo en el hilo que saldrá del hilo. Vea el enlace adjunto para una posible implementación.

Mata un hilo en Python

Es mejor si no matas un hilo. Una forma podría ser introducir un " intentar " bloquee en el ciclo del hilo y arroje una excepción cuando desee detener el hilo (por ejemplo, un descanso / retorno / ... que detiene su for / while / ...). He usado esto en mi aplicación y funciona ...

Definitivamente es posible implementar un método Thread.stop como se muestra en el siguiente código de ejemplo:

import sys
import threading
import time


class StopThread(StopIteration):
    pass

threading.SystemExit = SystemExit, StopThread


class Thread2(threading.Thread):

    def stop(self):
        self.__stop = True

    def _bootstrap(self):
        if threading._trace_hook is not None:
            raise ValueError('Cannot run thread with tracing!')
        self.__stop = False
        sys.settrace(self.__trace)
        super()._bootstrap()

    def __trace(self, frame, event, arg):
        if self.__stop:
            raise StopThread()
        return self.__trace


class Thread3(threading.Thread):

    def _bootstrap(self, stop_thread=False):
        def stop():
            nonlocal stop_thread
            stop_thread = True
        self.stop = stop

        def tracer(*_):
            if stop_thread:
                raise StopThread()
            return tracer
        sys.settrace(tracer)
        super()._bootstrap()

###############################################################################


def main():
    test1 = Thread2(target=printer)
    test1.start()
    time.sleep(1)
    test1.stop()
    test1.join()
    test2 = Thread2(target=speed_test)
    test2.start()
    time.sleep(1)
    test2.stop()
    test2.join()
    test3 = Thread3(target=speed_test)
    test3.start()
    time.sleep(1)
    test3.stop()
    test3.join()


def printer():
    while True:
        print(time.time() % 1)
        time.sleep(0.1)


def speed_test(count=0):
    try:
        while True:
            count += 1
    except StopThread:
        print('Count =', count)

if __name__ == '__main__':
    main()

La clase Thread3 parece ejecutar código aproximadamente un 33% más rápido que la clase Thread2 .

from ctypes import *
pthread = cdll.LoadLibrary("libpthread-2.15.so")
pthread.pthread_cancel(c_ulong(t.ident))

t es su objeto Thread .

Lea la fuente de Python ( Modules / threadmodule.c y Python / thread_pthread.h ) puede ver que Thread.ident es un pthread_t , para que pueda hacer cualquier cosa que pthread pueda hacer en python, use libpthread .

La siguiente solución alternativa se puede usar para matar un hilo:

kill_threads = False

def doSomething():
    global kill_threads
    while True:
        if kill_threads:
            thread.exit()
        ......
        ......

thread.start_new_thread(doSomething, ())

Esto puede usarse incluso para terminar hilos, cuyo código está escrito en otro módulo, desde el hilo principal. Podemos declarar una variable global en ese módulo y usarla para terminar los hilos generados en ese módulo.

Usualmente uso esto para terminar todos los hilos en la salida del programa. Esta podría no ser la manera perfecta de terminar hilos / s pero podría ayudar.

Una cosa que quiero agregar es que si lee la documentación oficial en threading lib Python , se recomienda evitar el uso de " demonic " subprocesos, cuando no desea que los subprocesos terminen abruptamente, con la bandera que Paolo Rovelli mencionó .

De la documentación oficial:

  

Los hilos del demonio se detienen abruptamente al apagarse. Es posible que sus recursos (como archivos abiertos, transacciones de bases de datos, etc.) no se liberen correctamente. Si desea que sus hilos se detengan con gracia, hágalos no demoníacos y utilice un mecanismo de señalización adecuado, como un Evento.

Creo que crear hilos demoníacos depende de su aplicación, pero en general (y en mi opinión) es mejor evitar matarlos o hacerlos demoníacos. En el multiprocesamiento, puede usar is_alive () para verificar el estado del proceso y " terminar " para terminarlos (también evitas problemas GIL). Pero puede encontrar más problemas, a veces, cuando ejecuta su código en Windows.

Y recuerda siempre que si tienes "hilos en vivo", el intérprete de Python se ejecutará para esperarlos. (Debido a esto daemonic puede ayudarte si no importa abruptamente termina).

Si está llamando explícitamente a time.sleep () como parte de su hilo (digamos sondear algún servicio externo), una mejora con respecto al método de Phillipe es usar el tiempo de espera en el evento método wait () donde sea que sleep()

Por ejemplo:

import threading

class KillableThread(threading.Thread):
    def __init__(self, sleep_interval=1):
        super().__init__()
        self._kill = threading.Event()
        self._interval = sleep_interval

    def run(self):
        while True:
            print("Do Something")

            # If no kill signal is set, sleep for the interval,
            # If kill signal comes in while sleeping, immediately
            #  wake up and handle
            is_killed = self._kill.wait(self._interval)
            if is_killed:
                break

        print("Killing Thread")

    def kill(self):
        self._kill.set()

Luego para ejecutarlo

t = KillableThread(sleep_interval=5)
t.start()
# Every 5 seconds it prints:
#: Do Something
t.kill()
#: Killing Thread

La ventaja de usar wait () en lugar de sleep () ing y verificar regularmente el evento es que puede programar en intervalos de sueño más largos, el hilo se detiene casi de inmediato (cuando de otro modo estaría sleep () ing) y, en mi opinión, el código para manejar la salida es significativamente más simple.

Llegué tarde a este juego, pero he estado luchando con una pregunta similar y aparece lo siguiente para resolver el problema perfectamente para mí Y me permite hacer una comprobación básica del estado del hilo y limpiarlo cuando sale el subproceso demonizado:

import threading
import time
import atexit

def do_work():

  i = 0
  @atexit.register
  def goodbye():
    print ("'CLEANLY' kill sub-thread with value: %s [THREAD: %s]" %
           (i, threading.currentThread().ident))

  while True:
    print i
    i += 1
    time.sleep(1)

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

def after_timeout():
  print "KILL MAIN THREAD: %s" % threading.currentThread().ident
  raise SystemExit

threading.Timer(2, after_timeout).start()

Rendimientos:

0
1
KILL MAIN THREAD: 140013208254208
'CLEANLY' kill sub-thread with value: 2 [THREAD: 140013674317568]

Hay una biblioteca creada para este propósito, stopit . Aunque todavía se aplican algunas de las mismas precauciones enumeradas en este documento, al menos esta biblioteca presenta una técnica regular y repetible para lograr el objetivo establecido.

Esto parece funcionar con pywin32 en Windows 7

my_thread = threading.Thread()
my_thread.start()
my_thread._Thread__stop()
  

Esta es una mala respuesta, vea los comentarios

Aquí se explica cómo hacerlo:

from threading import *

...

for thread in enumerate():
    if thread.isAlive():
        try:
            thread._Thread__stop()
        except:
            print(str(thread.getName()) + ' could not be terminated'))

Déle unos segundos, entonces su hilo debería detenerse. Compruebe también el método thread._Thread__delete () .

Recomiendo un método thread.quit () por conveniencia. Por ejemplo, si tiene un socket en su hilo, recomendaría crear un método quit () en su clase de socket-handle, terminar el socket y luego ejecutar un hilo ._Thread__stop () dentro de su quit () .

Inicie el subproceso con setDaemon (True).

def bootstrap(_filename):
    mb = ModelBootstrap(filename=_filename) # Has many Daemon threads. All get stopped automatically when main thread is stopped.

t = threading.Thread(target=bootstrap,args=('models.conf',))
t.setDaemon(False)

while True:
    t.start()
    time.sleep(10) # I am just allowing the sub-thread to run for 10 sec. You can listen on an event to stop execution.
    print('Thread stopped')
    break

Si bien es bastante antiguo, esto puede ser útil solución para algunos:

  

Un pequeño módulo que extiende la funcionalidad del módulo de subprocesos -   permite que un hilo genere excepciones en el contexto de otro   hilo. Al subir SystemExit , finalmente puede matar los hilos de Python.

import threading
import ctypes     

def _async_raise(tid, excobj):
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(excobj))
    if res == 0:
        raise ValueError("nonexistent thread id")
    elif res > 1:
        # """if it returns a number greater than one, you're in trouble, 
        # and you should call it again with exc=NULL to revert the effect"""
        ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, 0)
        raise SystemError("PyThreadState_SetAsyncExc failed")

class Thread(threading.Thread):
    def raise_exc(self, excobj):
        assert self.isAlive(), "thread must be started"
        for tid, tobj in threading._active.items():
            if tobj is self:
                _async_raise(tid, excobj)
                return

        # the thread was alive when we entered the loop, but was not found 
        # in the dict, hence it must have been already terminated. should we raise
        # an exception here? silently ignore?

    def terminate(self):
        # must raise the SystemExit type, instead of a SystemExit() instance
        # due to a bug in PyThreadState_SetAsyncExc
        self.raise_exc(SystemExit)

Por lo tanto, permite que un hilo '' genere excepciones en el contexto de otro hilo '' y de esta manera, el subproceso terminado puede manejar la terminación sin verificar regularmente un indicador de aborto.

Sin embargo, de acuerdo con su fuente original , hay algunos problemas con este código.

  
      
  • La excepción se generará solo cuando se ejecute el código de bytes de Python. Si su hilo llama a una función de bloqueo nativa / incorporada, el   la excepción se generará solo cuando la ejecución regrese a la pitón   código.      
        
    • También hay un problema si la función integrada llama internamente a PyErr_Clear (), lo que efectivamente cancelaría su excepción pendiente.   Puedes intentar subirlo de nuevo.
    •   
  •   
  • Solo los tipos de excepción se pueden generar de forma segura. Es probable que las instancias de excepción provoquen un comportamiento inesperado y, por lo tanto, estén restringidas.      
  •   
  • Pedí exponer esta función en el módulo de subprocesos incorporado, pero dado que ctypes se ha convertido en una biblioteca estándar (a partir de 2.5), y esto
      no es probable que la función sea independiente de la implementación, se puede mantener
      sin exponer.
  •   

Pieter Hintjens - uno de los fundadores del ØMQ -project - dice, usar ØMQ y evitar las primitivas de sincronización como bloqueos, mutexes, eventos, etc., es la forma más segura y segura de escribir programas multiproceso:

http://zguide.zeromq.org/py:all#Multithreading -with-ZeroMQ

Esto incluye decirle a un hilo secundario que debe cancelar su trabajo. Esto se haría equipando el hilo con un zócalo ØMQ y sondeando en ese zócalo un mensaje que indique que debería cancelarse.

El enlace también proporciona un ejemplo de código python multiproceso con ØMQ.

Puede ejecutar su comando en un proceso y luego matarlo usando la identificación del proceso. Necesitaba sincronizar entre dos hilos, uno de los cuales no regresa solo.

processIds = []

def executeRecord(command):
    print(command)

    process = subprocess.Popen(command, stdout=subprocess.PIPE)
    processIds.append(process.pid)
    print(processIds[0])

    #Command that doesn't return by itself
    process.stdout.read().decode("utf-8")
    return;


def recordThread(command, timeOut):

    thread = Thread(target=executeRecord, args=(command,))
    thread.start()
    thread.join(timeOut)

    os.kill(processIds.pop(), signal.SIGINT)

    return;

Si realmente necesita la capacidad de eliminar una subtarea, use una implementación alternativa. multiprocesamiento y gevent ambos admiten matar indiscriminadamente un " thread " ;.

El subproceso de Python no admite la cancelación. Ni lo intentes. Es muy probable que su código se bloquee, corrompa o pierda memoria, o tenga otros "interesantes" involuntarios efectos difíciles de depurar que ocurren raramente y de forma no determinante.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top