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

987

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


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

844

在Python中,以及任何其他语言中,突然终止线程通常是一个不好的模式。考虑以下情况:

  • 线程正在持有必须正确关闭的关键资源
  • 线程已创建了多个其他线程,这些线程也必须被终止

如果您可以承担得起(如果您正在管理自己的线程),处理这种情况的好方法是拥有一个退出请求标志,每个线程定期检查该标志,以确定是否是退出的时候。

例如:

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,  *args, **kwargs):
        super(StoppableThread, self).__init__(*args, **kwargs)
        self._stop_event = threading.Event()

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

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

在这段代码中,当你想要线程退出时,你应该在线程上调用stop(),并使用join()等待线程正确退出。线程应该定期检查停止标志。
然而,有些情况下,你确实需要终止一个线程。一个例子是当你封装一个执行耗时调用的外部库,并且你想要中断它。
以下代码允许(在一些限制下)在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 an 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 raise_exc(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.raise_exc( SomeException )
            while t.isAlive():
                time.sleep( 0.1 )
                t.raise_exc( 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 exception in the context of the
        thread represented by this instance.
        """
        _async_raise( self._get_my_tid(), exctype )

(基于Tomer Filiba的《可终止线程》。关于PyThreadState_SetAsyncExc返回值的引用似乎来自于一个旧版本的Python。)
正如文档中所指出的,这并不是一个万能的解决办法,因为如果线程在Python解释器之外忙碌,它将无法捕获中断。
这段代码的一个好的使用模式是让线程捕获特定的异常并执行清理操作。这样,您可以中断一个任务并仍然进行适当的清理工作。

133
此外,我不确定关于线程不应该被突然终止的论点,“因为该线程可能持有必须被正确关闭的关键资源”,这同样适用于主程序。主程序也可以被用户突然终止(例如,在Unix中使用Ctrl-C),在这种情况下,它们尽可能友好地处理这种可能性。因此,我无法理解线程有何特殊之处,以及为什么它们不应该像主程序一样接受相同的处理方式(即可以被突然终止)。 :) 能否详细说明一下? - Eric O. Lebigot
22
另一方面,如果线程拥有的所有资源都是本地资源(例如打开的文件、套接字),Linux 在进程清理方面表现得相当不错,不会发生泄漏。不过我遇到过这样的情况,使用套接字创建了一个服务器,如果我使用 Ctrl-C 进行强制中断,就无法再启动程序,因为它无法绑定套接字。我需要等待 5 分钟。正确的解决方案是捕获 Ctrl-C 并进行干净的套接字断开。 - Philippe F
12
顺便提一下,你可以使用SO_REUSEADDR套接字选项来避免出现“地址已在使用”的错误。 - Messa
13
关于这个答案的说明:至少对于我(py2.6),在 res != 1 的情况下,我必须传递 None 而不是 0,并且我必须调用 ctypes.c_long(tid) 并将其直接传递给任何 ctypes 函数,而不是直接传递 tid。 - Walt W
23
值得一提的是,在Python 3线程库中已经使用了_stop变量名。因此,如果不使用其他变量名,你会遇到错误。 - diedthreetimes
显示剩余31条评论

175

multiprocessing.Process可以使用p.terminate()方法终止进程。

如果我想要杀死一个线程,但是不想使用标志、锁、信号量、事件或其他方式,我会将该线程升级为完整的进程。对于只使用少量线程的代码,开销并不大。

例如,这对于易于终止执行阻塞I/O的帮助程序“线程”非常有用。

转换很简单:在相关代码中,将所有threading.Thread替换为multiprocessing.Process,将所有queue.Queue替换为multiprocessing.Queue并在父进程中添加需要调用p.terminate()来杀死其子进程p的必需语句。

请参阅Python文档

示例:

import multiprocessing
proc = multiprocessing.Process(target=your_proc_function, args=())
proc.start()
# Terminate the process
proc.terminate()  # sends a SIGTERM

谢谢。我用multiprocessing.JoinableQueue替换了queue.Queue,并按照这个答案操作:https://dev59.com/X2ct5IYBdhLWcg3wk-Vq#11984760 - David Braun
29
multiprocessing 很好用,但需要注意的是参数会被序列化到新进程中。所以如果其中一个参数是无法序列化的(比如 logging.log),使用 multiprocessing 可能不是个好主意。 - Lyager
8
在Windows系统中,multiprocessing参数被序列化并传递给新的进程,但是在Linux系统中会使用forking方法复制它们(Python 3.7版本如此,其他版本未知)。因此,你可能会得到在Linux上正常运行但在Windows上引发pickle错误的代码。 - nyanpasu64
使用带有日志记录的多进程编程是棘手的。需要使用QueueHandler(请参见此教程)。我是通过吃亏才学会的。 - Fanchen Bao
很遗憾我无法监视在多进程中运行的函数...谢谢。 - Adonis

139

没有官方API可以做到这一点。

您需要使用平台API来终止线程,例如pthread_kill或TerminateThread。您可以通过pythonwin或ctypes访问此类API。

请注意,这本质上是不安全的。它可能会导致无法收集的垃圾(来自成为垃圾的堆栈帧的局部变量),并且如果在杀死线程时该线程具有GIL,则可能会导致死锁。


48
如果涉及的线程持有GIL,那么这将导致死锁。 - Matthias Urlichs

104
如果你想要终止整个程序,你可以将线程设置为“守护线程”。参见Thread.daemon

这没有任何意义。文档明确说明:“必须在调用start()之前设置,否则将引发RuntimeError。”因此,如果我想杀死一个最初不是守护进程的线程,我该怎么做? - khatchad
42
Raffi,我认为他在建议你提前设置,知道当你的主线程退出时,你也希望守护线程退出。 - fantabolous
3
将线程设置为守护进程是为了让线程在主程序关闭时继续运行,这是您会考虑这样做的原因吗? - Michele Piccolini
6
相反,守护线程在其他线程结束后不会保持进程运行。 - Davis Herring
2
这对我来说是最好的答案,我只想在父进程关闭时清理线程。谢谢! - Lin Meyer
显示剩余2条评论

89

就像其他人提到的那样,通常会设置一个停止标志。对于一些轻量级的情况(没有线程的子类化,没有全局变量),可以使用 lambda 回调函数进行处理。(请注意,在 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()

print()替换为一个总是刷新(sys.stdout.flush())的pr()函数可能会提高Shell输出的精度。

(仅在Windows/Eclipse/Python3.3上测试过)


3
在Linux / Python 2.7上验证过,运行得非常好。这应该是官方答案,而且更简单。 - Paul Kenjora
3
在Linux Ubuntu Server 17.10/Python 3.6.3上验证通过并且可行。 - Marcos
pr() 函数是什么? - alper
1
@alper,你可以创建一个新的函数,它的功能类似于“print”函数,但会刷新输出,并将其命名为“pr”。 - Pyzard
已在Windows 10/Python 3.9.12上验证,且运行完美。 - robbinc91

53
在Python中,你不能直接杀死一个线程。
如果你真的不需要一个线程,而是想使用多进程包multiprocessing package ,你可以调用方法来终止一个进程。
your_process.terminate()  # kill the process!

Python会终止您的进程(在Unix上通过SIGTERM信号,在Windows上通过TerminateProcess()调用)。在使用队列或管道时要注意使用它!(它可能会破坏队列/管道中的数据)
请注意,multiprocessing.Event和multiprocessing.Semaphore的工作方式与threading.Event和threading.Semaphore完全相同。实际上,前者是后者的克隆。
如果您真的需要使用线程,没有直接终止它的方法。但是,您可以使用"守护线程"。实际上,在Python中,线程可以被标记为守护线程:
your_thread.daemon = True  # set the Thread as a "daemon thread"

主程序将在没有活动的非守护线程时退出。换句话说,当您的主线程(当然是非守护线程)完成其操作时,即使仍然有一些守护线程在工作,程序也会退出。
请注意,在调用start()方法之前,将线程设置为daemon是必要的!
当然,您可以并且应该在multiprocessing中使用daemon。在这里,当主进程退出时,它会尝试终止所有守护子进程。
最后,请注意,sys.exit()os.kill()不是选择。

我不知道为什么人们不投票支持这个。这个答案有什么问题吗?虽然这个对我有效。 - fsevenm
5
@fsevenm说:进程与线程是相同的。它们在独立的内存空间中运行,因此没有简单的共享全局变量。传递参数涉及将它们进行pickle并在另一侧进行unpickle。加上启动和运行单独进程的开销,需要比仅切换线程时更多的额外开销。从许多方面来看,这就像苹果和橙子,所以这可能是为什么来回答你的问题。 - martineau
@martineau 我从来没有说它们是相同的东西。我实际上是从一个“如果你不真正需要一个线程”的角度开始的,因为并不总是这样的情况,然后继续使用一个“如果你真的需要使用一个线程”的角度... - Paolo Rovelli
@PaoloRovelli:在我评论的第一部分中,我的意思是写“进程不同于线程”。 - martineau

45

这是基于thread2 -- 可停止的线程 ActiveState食谱。

你需要调用PyThreadState_SetAsyncExc(),该函数只能通过ctypes模块使用。

这仅在Python 2.7.3上进行了测试,但很可能适用于其他不久前的2.x版本。 PyThreadState_SetAsyncExc()在Python 3中仍然存在以保持向后兼容性(但我没有测试过)。

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

我使用类似这样的方法来给我的线程一个 KeyboardInterrupt 信号,以便它们有机会进行清理。如果在此之后它们仍然挂起,那么 SystemExit 是适当的,或者可以从终端杀死该进程。 - drevicko
如果线程当前正在执行,则此方法有效。如果线程在系统调用中,则此方法无效;异常将被静默忽略。 - Matthias Urlichs
1
@JohanDahlin 你可以稍等一下(如果你想重试,无论如何你都需要这样做),然后进行isAlive()测试。无论如何,虽然这样做是可行的,但我也不能保证它不会留下悬空引用。虽然在理论上可以通过谨慎使用pthread_cleanup_push()/_pop()来使线程终止在CPython中变得安全,但要正确实现它需要大量的工作,并且会明显减慢解释器的速度。 - Matthias Urlichs
Thread.isAlive()方法已经不存在了。应该用is_alive()来替代它。请参考:https://docs.python.org/3/library/threading.html#threading.Thread.is_alive - undefined

35

强制杀死线程时,应该与其合作而不是单方面操作。

强制终止线程会破坏try/finally代码块设置的所有保证,可能会导致锁定锁住、文件未关闭等问题。

唯一可以争辩认为强制杀死线程是个好主意的时候是为了快速结束程序,但永远不要单独终止某个线程。


32
为什么只是让一个线程在完成当前的循环后自行终止,这件事那么难?我不理解。 - Mehdi
5
CPU没有内置机制来识别"循环",最好的方法是使用一些信号,当前在循环内部的代码将在退出循环时检查这些信号。处理线程同步的正确方式是通过合作手段实现,线程的挂起、恢复和终止是调试器和操作系统的功能,而不是应用程序代码的功能。 - Lasse V. Karlsen
5
@Mehdi:如果我(个人)在线程中编写代码,是的,我同意你的观点。但有些情况下,我正在运行第三方库,而我无法访问该代码的执行循环。这是所请求功能的一个用例。 - Dan H
1
@DanH,当涉及到第三方代码时情况会更糟,因为你不知道它可能会造成什么样的损害。如果你的第三方库不够健壮,需要被终止,那么你应该采取以下措施之一:(1)请求作者修复问题,(2)使用其他库。如果你真的没有选择,那么将该代码放入一个独立的进程中应该更安全,因为某些资源仅在单个进程内共享。 - Phil1970
如果我的应用程序中有连接线程,并且我想关闭它。而且它是一个守护进程。 那么我怎样才能最终关闭它呢?我不是要关闭应用程序,我只需要取消连接即可。 - aleXela
@aleXela,你需要做的就是关闭连接对象,这样线程就会失败。或者,你可以添加一个信号,让线程也检查它,这样它就能平稳地终止。 - Lasse V. Karlsen

28
如果你在你的线程中明确调用time.sleep()(比如轮询一些外部服务),那么对于Phillipe的方法的改进是在你的sleep()语句处使用eventwait()方法的超时时间。例如:
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()

然后运行它。
t = KillableThread(sleep_interval=5)
t.start()
# Every 5 seconds it prints:
#: Do Something
t.kill()
#: Killing Thread

使用wait()而不是sleep()并定期检查事件的优点在于,您可以编程更长的睡眠间隔,线程几乎立即停止(当您将否则处于sleep()状态时),并且在我看来,处理退出的代码显着更简单。

5
为什么这篇帖子被踩了?这篇帖子有什么问题吗?它看起来正是我所需要的... - JDOaktown
虽然这篇文章不是我所需要的(我需要安全地从子进程中断父进程),但我在代码的其他部分中肯定使用了 time.sleep 并将轮询间隔缩小,以便我的脚本能够更快地响应。然而,这个解决方案具有使轮询间隔变小的所有好处,却没有浪费计算资源的缺点。+1 非常感谢。 - A Kareem

20

您可以通过在将退出线程的跟踪中安装跟踪来终止线程。 可以查看附带链接以获取一种可能的实现方式。

在Python中终止线程


2
这里的少数答案之一,实际上是有效的。 - Ponkadoodle
8
这个解决方案有两个问题:(a) 使用sys.settrace()安装跟踪器会使你的线程运行变慢。如果它是计算密集型任务,可能会慢10倍。 (b) 在系统调用期间,不会影响你的线程。 - Matthias Urlichs
链接的代码存在另一个问题,它覆盖了 start() 方法,而当前文档明确指出“换句话说,只有在定义子类时才覆盖此类的 __init__()run() 方法”。 - martineau

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