有没有办法终止一个线程?

987

是否可能在不设置/检查任何标志/信号量等的情况下终止正在运行的线程?


特别是,在线程中是否有一种方法可以生成类似于KeyboardInterrupt的异常? - Josiah Yoder
31个回答

13
可以在以下示例代码中实现一个Thread.stop方法,这是绝对可行的:
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()
Thread3类似乎比Thread2类运行代码快大约33%。
附录:

如果对Python的C API有足够的了解,并且使用ctypes模块,就可以以更高效的方式在需要时停止线程。使用sys.settrace的问题在于跟踪函数在每条指令之后运行。如果在需要中止的线程上引发异步异常,就不会产生执行速度的惩罚。以下代码在这方面提供了一些灵活性:

#! /usr/bin/env python3
import _thread
import ctypes as _ctypes
import threading as _threading

_PyThreadState_SetAsyncExc = _ctypes.pythonapi.PyThreadState_SetAsyncExc
# noinspection SpellCheckingInspection
_PyThreadState_SetAsyncExc.argtypes = _ctypes.c_ulong, _ctypes.py_object
_PyThreadState_SetAsyncExc.restype = _ctypes.c_int

# noinspection PyUnreachableCode
if __debug__:
    # noinspection PyShadowingBuiltins
    def _set_async_exc(id, exc):
        if not isinstance(id, int):
            raise TypeError(f'{id!r} not an int instance')
        if not isinstance(exc, type):
            raise TypeError(f'{exc!r} not a type instance')
        if not issubclass(exc, BaseException):
            raise SystemError(f'{exc!r} not a BaseException subclass')
        return _PyThreadState_SetAsyncExc(id, exc)
else:
    _set_async_exc = _PyThreadState_SetAsyncExc


# noinspection PyShadowingBuiltins
def set_async_exc(id, exc, *args):
    if args:
        class StateInfo(exc):
            def __init__(self):
                super().__init__(*args)

        return _set_async_exc(id, StateInfo)
    return _set_async_exc(id, exc)


def interrupt(ident=None):
    if ident is None:
        _thread.interrupt_main()
    else:
        set_async_exc(ident, KeyboardInterrupt)


# noinspection PyShadowingBuiltins
def exit(ident=None):
    if ident is None:
        _thread.exit()
    else:
        set_async_exc(ident, SystemExit)


class ThreadAbortException(SystemExit):
    pass


class Thread(_threading.Thread):
    def set_async_exc(self, exc, *args):
        return set_async_exc(self.ident, exc, *args)

    def interrupt(self):
        self.set_async_exc(KeyboardInterrupt)

    def exit(self):
        self.set_async_exc(SystemExit)

    def abort(self, *args):
        self.set_async_exc(ThreadAbortException, *args)

5
这是一种巧妙的方式来检测线程中是否设置了 self.__stop。需要注意的是,就像这里的其他解决方案一样,它实际上不会打断阻塞调用,因为跟踪函数只有在进入新的局部作用域时才被调用。值得注意的是,sys.settrace 主要用于实现调试器、剖析等,因此被认为是 CPython 的一个实现细节,并不能保证在其他 Python 实现中存在。 - dano
5
使用Thread2类的最大问题之一是其运行代码速度大约慢了十倍。可能仍有一些人认为这是可以接受的。 - Noctis Skytower
1
+1 这会显著减慢代码的执行速度。我建议这个解决方案的作者在答案中包含这些信息。 - Vishal
@Vishal,请查看答案的新附录,以获得更快的实施方式。 - undefined

12

最好不要强制终止线程。 一个方法是在线程的循环中引入“try”块,并在想要停止线程时抛出异常(例如,使用break/return/...停止for/while/...)。 我在我的应用程序中使用过这种方法,它很有效...


10

我知道现在已经晚了,但我一直在困扰一个类似的问题,下面的方法似乎能完美地解决我的问题,并且让我在子线程退出时进行一些基本的线程状态检查和清理:

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()

产生:

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

1
在这个例子中,为什么在“after_timeout”线程上引发“SystemExit”会对主线程(它只是等待前者退出)产生影响? - Davis Herring
@DavisHerring,我不确定你的意思。SystemExit会终止主线程,你为什么认为它不会对主线程产生任何影响?如果没有这个调用,程序将继续等待子线程。你也可以使用ctrl+c或其他方式来终止主线程,但这只是一个例子。 - slumtrimpet
1
@slumtrimpet:SystemExit 只有两个特殊属性:它不会产生回溯(当任何线程通过抛出异常退出时),并且如果 线程通过抛出异常退出,它将设置退出状态(同时仍然等待其他非守护线程退出)。 - Davis Herring
主线程继续工作,并没有被子线程引发的 SystemExit 中断。必须通过终端使用 kill -9 终止脚本。 - A Kareem

8

以下方法可用于终止一个线程:

kill_threads = False

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

thread.start_new_thread(doSomething, ())

即使线程的代码是写在另一个模块中的,也可以使用此方法来终止线程。我们可以在该模块中声明一个全局变量,并使用它来终止在该模块中生成的线程。

我通常使用此方法在程序退出时终止所有线程。这可能不是终止线程的完美方式,但可以有所帮助。


在Python 3.6+中,'Thread'对象没有'exit'属性。 - skjerns
不要使用线程退出,只需打破循环即可退出线程。 - PanDe

8

这里有另一种实现方式,用极其简洁的代码来完成。它适用于2021年Python 3.7:

import ctypes 

def kill_thread(thread):
    """
    thread: a threading.Thread object
    """
    thread_id = thread.ident
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(SystemExit))
    if res > 1:
        ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
        print('Exception raise failure')

翻译自这里:https://www.geeksforgeeks.org/python-different-ways-to-kill-a-thread/

本文介绍了在Python中关闭线程的不同方法。


7
如果你正在使用Python 3.7版本,那么你一定是来自2018年的时间旅行者。如果您要参考2021年,请使用Python 3.9进行测试。 PyThreadState_SetAsyncExc方法只是对线程退出进行“调度”。它不会杀死线程,特别是当它正在执行外部C库时。尝试使用该方法终止sleep(100)。它将在100秒后被“终止”。它与while flag: -> flag = False方法一样有效。 - sausix
对我来说没起作用。没有错误或异常,线程正常运行。 - Rishabh Gupta

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

t 是您的 Thread 对象。

阅读 Python 源代码 (Modules/threadmodule.cPython/thread_pthread.h),您可以看到 Thread.ident 是一个 pthread_t 类型,因此您可以在 Python 中使用 libpthread 执行任何 pthread 可以执行的操作。


13
无论是在Windows还是Linux系统上,你都不应该这样做。原因是:当你尝试这样做时,相关的线程可能会持有全局解释器锁(GIL),导致你的程序立即死锁。即使线程没有持有GIL,也不能保证finally块中的代码会得到执行,因此这是一个非常不安全的想法。 - Matthias Urlichs
在ipython中尝试过这个,整个ipython进程都会挂起。 - user4918159

4

有一个专门用于此目的的库,stopit。尽管仍然存在一些相同的注意事项,但至少该库提供了一种常规、可重复的技术来实现所述目标。


4
我想补充一点的是,如果你在阅读Python线程库的官方文档时,建议避免使用“恶魔”线程,当你不希望线程突然结束时,可以使用Paolo Rovelli提到的标志。
从官方文档中得知:
守护线程在关闭时会被突然停止。它们的资源(如打开的文件、数据库事务等)可能无法正确释放。如果您希望您的线程正常停止,请将它们设置为非守护线程,并使用适当的信号机制,例如事件。
我认为创建守护线程取决于您的应用程序,但通常最好避免杀死它们或使它们成为守护线程(在我看来)。在多进程中,您可以使用is_alive()检查进程状态并使用“终止”来完成它们(还可以避免GIL问题)。但有时,在Windows中执行代码时可能会遇到更多问题。
并且永远记住,如果您有“活着的线程”,Python解释器将运行以等待它们。(因此,如果不关心突然结束,守护线程可以帮助您)。

@Tshepang 这意味着,如果您的应用程序中有任何正在运行的非守护线程,Python 解释器将继续运行,直到所有非守护线程都完成。如果您不关心线程在程序终止时是否结束,则将它们设置为守护线程可能会有用。 - Tom Myddeltyn

2
尽管它有些老旧,this 对于某些人来说可能是一个方便的解决方案:
一个小模块扩展了线程模块的功能——允许一个线程在另一个线程的上下文中引发异常。通过引发 SystemExit,您最终可以终止 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)

因此,它允许“一个线程在另一个线程的上下文中引发异常”,这样,终止的线程可以处理终止而不需要定期检查中止标志。然而,根据其原始来源,这段代码存在一些问题。
异常只会在执行 Python 字节码时被引发。如果您的线程调用本地/内置阻塞函数,则异常仅在执行返回到 Python 代码时被引发。
如果内置函数在内部调用 PyErr_Clear(),那么您的待处理异常将被有效地取消。您可以尝试再次引发它。
只有异常类型可以安全地引发。异常实例可能会导致意外行为,因此受到限制。
例如:t1.raise_exc(TypeError) 而不是 t1.raise_exc(TypeError("blah"))。
我要求在内置线程模块中公开此功能,但自从 ctypes 成为标准库(2.5 版本)以来,这个特性不太可能是与实现无关的,因此可能会保持未公开状态。

2

为了进一步完善@SCB的想法(这正是我所需要的),创建一个KillableThread子类,并使用自定义函数:

from threading import Thread, Event

class KillableThread(Thread):
    def __init__(self, sleep_interval=1, target=None, name=None, args=(), kwargs={}):
        super().__init__(None, target, name, args, kwargs)
        self._kill = Event()
        self._interval = sleep_interval
        print(self._target)

    def run(self):
        while True:
            # Call custom function with arguments
            self._target(*self._args)

            # 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()

if __name__ == '__main__':

    def print_msg(msg):
        print(msg)

    t = KillableThread(10, print_msg, args=("hello world"))
    t.start()
    time.sleep(6)
    print("About to kill thread")
    t.kill()

自然地,就像 @SBC 一样,线程不会等待运行新的循环来停止。在这个例子中,你会看到“Killing Thread”消息被打印,紧接着是“About to kill thread”,而不是等待4秒钟让线程完成(因为我们已经睡了6秒钟)。

KillableThread 构造函数的第二个参数是你的自定义函数(这里是 print_msg)。Args 参数是调用函数时将使用的参数(这里是“hello world”)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接