Python 中杀死线程的几个方法

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

通常,突然终止线程被认为是一种糟糕的编程习惯。突然终止线程可能会使必须正确关闭的关键资源处于打开状态。但是您可能希望在某个特定时间段过去或产生某个中断后终止线程。

下面有6种方式杀死线程,其中前两种较常用,我在原文的基础上进行了一些修改,以覆盖更多的情况。

  • 在 python 线程中抛出异常
  • 使用 flag / Event()
  • 使用 traces 杀死线程
  • 使用 multiprocessing module 杀死线程
  • 通过将其设置为守护进程来杀死 Python 线程
  • 使用隐藏函数 _stop()

0x01 在 python 线程中抛出异常

此方法使用函数 PyThreadState_SetAsyncExc() 在线程中引发异常。

这种方法主要分两步。第一步获取线程id,第二步调用 SetAsyncExc,另外可以通过返回值查看是否杀死成功。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# Python program raising
# exceptions in a python
# thread

import threading
import ctypes
import time

class thread_with_exception(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name

def run(self):

# target function of the thread class
try:
while True:
print('running ' + self.name)
finally:
print('ended')

def get_id(self):

# returns id of the respective thread
if hasattr(self, '_thread_id'):
return self._thread_id
for id, thread in threading._active.items():
if thread is self:
return id

def raise_exception(self):
thread_id = self.get_id()
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')

t1 = thread_with_exception('Thread 1')
t1.start()
time.sleep(2)
t1.raise_exception()
t1.join()

另外,这里还有另一种方法获取线程的 id:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import threading
import time
import inspect
import ctypes


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(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")


def stop_thread(thread):
_async_raise(thread.ident, SystemExit)


class TestThread(threading.Thread):
def run(self):
print("begin run the child thread")
while True:
print("sleep 1s")
time.sleep(1)


if __name__ == "__main__":
print("begin run main thread")
t = TestThread()
t.start()
time.sleep(3)
stop_thread(t)
print("main thread end")

当我们在一台机器上运行上面的代码时,你会注意到,只要函数 raise_exception() 被调用,目标函数 run() 就会结束。这是因为一旦引发异常,程序控制就会跳出 try 块,run() 函数将终止。之后可以调用 join() 函数来终止线程。在没有函数 run_exception() 的情况下,目标函数 run() 将一直运行,并且永远不会调用 join() 函数来终止线程。   

0x02 使用 flag

为了杀死一个线程,我们可以声明一个停止标志,这个标志会被线程检查。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Python program showing
# how to kill threads
# using set/reset stop
# flag

import threading
import time

def run():
while True:
print('thread running')
global stop_threads
if stop_threads:
break

stop_threads = False
t1 = threading.Thread(target = run)
t1.start()
time.sleep(1)
stop_threads = True
t1.join()
print('thread killed')

在上面的代码中,一旦设置了全局变量 stop_threads,目标函数 run() 就会结束,并且可以使用 t1.join() 杀死线程 t1。但是由于某些原因,人们可能会避免使用全局变量。对于这些情况,可以传递函数对象以提供类似的功能,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Python program killing
# threads using stop
# flag

import threading
import time

def run(stop):
while True:
print('thread running')
if stop():
break

def main():
stop_threads = False
t1 = threading.Thread(target = run, args =(lambda : stop_threads, ))
t1.start()
time.sleep(1)
stop_threads = True
t1.join()
print('thread killed')
main()

上面代码中传入的函数对象总是返回局部变量 stop_threads 的值。这个值在函数 run() 中被检查,一旦 stop_threads 被重置,run() 函数结束并且线程可以被杀死。

另外,使用 threading.Event() 可以更优雅的实现这一功能。

0x03 使用 traces 杀死线程

此方法通过在每个线程中使用 traces 来工作。每个 trace 都会在检测到某些刺激或标志时自行终止,从而立即终止关联的线程。例如

Python program using
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# traces to kill threads

import sys
import trace
import threading
import time
class thread_with_trace(threading.Thread):
def __init__(self, *args, **keywords):
threading.Thread.__init__(self, *args, **keywords)
self.killed = False

def start(self):
self.__run_backup = self.run
self.run = self.__run
threading.Thread.start(self)

def __run(self):
sys.settrace(self.globaltrace)
self.__run_backup()
self.run = self.__run_backup

def globaltrace(self, frame, event, arg):
if event == 'call':
return self.localtrace
else:
return None

def localtrace(self, frame, event, arg):
if self.killed:
if event == 'line':
raise SystemExit()
return self.localtrace

def kill(self):
self.killed = True

def func():
while True:
print('thread running')

t1 = thread_with_trace(target = func)
t1.start()
time.sleep(2)
t1.kill()
t1.join()
if not t1.isAlive():
print('thread killed')

在这段代码中,start() 被稍微修改为使用 settrace() 设置系统跟踪功能。本地跟踪函数的定义是,无论何时设置相应线程的终止标志(已终止),都会引发 SystemExit 异常执行下一行代码,结束目标函数func的执行。现在可以使用 join() 终止线程。

0x04 使用 multiprocessing module 杀死线程

Python 的multiprocessing module 允许您以类似于使用线程模块生成线程的方式生成进程。multiprocessing module 的接口与 threading 的接口类似。例如,在给定的代码中,我们创建了三个线程(进程),它们从 1 计数到 9。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Python program creating
# three threads

import threading
import time

# counts from 1 to 9
def func(number):
for i in range(1, 10):
time.sleep(0.01)
print('Thread ' + str(number) + ': prints ' + str(number*i))

# creates 3 threads
for i in range(0, 3):
thread = threading.Thread(target=func, args=(i,))
thread.start()

上述代码的功能也可以通过类似的方式使用多处理模块来实现,只需很少的改动。请参阅下面给出的代码。  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Python program creating
# thread using multiprocessing
# module

import multiprocessing
import time

def func(number):
for i in range(1, 10):
time.sleep(0.01)
print('Processing ' + str(number) + ': prints ' + str(number*i))

for i in range(0, 3):
process = multiprocessing.Process(target=func, args=(i,))
process.start()

尽管这两个模块的接口相似,但是这两个模块的实现却截然不同。所有线程共享全局变量,而进程彼此完全分离。因此,与杀死线程相比,杀死进程要安全得多。 Process 类提供了一种方法 terminate() 来终止进程。现在,回到最初的问题。假设在上面的代码中,我们想要在 0.03s 过去后杀死所有进程。此功能是使用以下代码中的 multiprocessing 实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Python program killing
# a thread using multiprocessing
# module

import multiprocessing
import time

def func(number):
for i in range(1, 10):
time.sleep(0.01)
print('Processing ' + str(number) + ': prints ' + str(number*i))

# list of all processes, so that they can be killed afterwards
all_processes = []

for i in range(0, 3):
process = multiprocessing.Process(target=func, args=(i,))
process.start()
all_processes.append(process)

# kill all processes after 0.03s
time.sleep(0.03)
for process in all_processes:
process.terminate()

虽然这两个模块有不同的实现。上面代码中多处理模块提供的这个功能类似于杀死线程。因此,只要我们需要在 Python 中实现线程终止,multiprocessing 就可以作为一个简单的替代方案。

0x05 通过将其设置为守护进程(daemon)来杀死 Python 线程

守护线程 是那些在主程序退出时被杀死的线程。例如

 

1
2
3
4
5
6
7
8
9
10
11
12
13
import threading
import time
import sys

def func():
while True:
time.sleep(0.5)
print("Thread alive, and it won't die on program termination")

t1 = threading.Thread(target=func)
t1.start()
time.sleep(2)
sys.exit()

请注意,线程 t1 保持活动状态并阻止主程序通过 sys.exit() 退出。在 Python 中,任何活动的非守护线程都会阻止主程序退出。然而,一旦主程序退出,守护线程本身就会被杀死。换句话说,主程序一退出,所有的守护线程就被杀死了。要将线程声明为守护进程,我们将关键字参数 daemon 设置为 True。例如,在给定的代码中,它演示了守护线程的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Python program killing
# thread using daemon

import threading
import time
import sys

def func():
while True:
time.sleep(0.5)
print('Thread alive, but it will die on program termination')

t1 = threading.Thread(target=func)
t1.daemon = True
t1.start()
time.sleep(2)
sys.exit()

请注意,一旦主程序退出,线程 t1 就会被终止。在程序终止可用于触发线程终止的情况下,此方法被证明非常有用。请注意,在 Python 中,只要所有非守护线程都死了,主程序就会终止,而不管有多少守护线程处于活动状态。因此,这些守护线程所持有的资源,例如打开的文件、数据库事务等,可能无法正常释放。 python 程序中的初始控制线程不是守护线程。除非确定知道这样做不会导致任何泄漏或死锁,否则不建议强行终止线程。

0x06 使用隐藏函数 _stop()

为了杀死一个线程,我们使用隐藏函数 _stop() 这个函数没有记录但可能会在下一版本的 python 中消失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Python program killing
# a thread using ._stop()
# function

import time
import threading

class MyThread(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(MyThread, self).__init__(*args, **kwargs)
self._stop = threading.Event()

# function using _stop function
def stop(self):
self._stop.set()

def stopped(self):
return self._stop.isSet()

def run(self):
while True:
if self.stopped():
return
print("Hello, world!")
time.sleep(1)

t1 = MyThread()

t1.start()
time.sleep(5)
t1.stop()
t1.join()

注意: 以上方法在某些情况下可能不起作用,因为 python 没有提供任何直接杀死线程的方法。