我經常被問到如何殺死一個后臺線程,這個問題的答案讓很多人不開心: 線程是殺不死的。在本文中,我將向您展示Python
中用于終止線程的兩個選項。
如果我們是一個好奇寶寶的話,可能會遇到這樣一個問題,就是:如何殺死一個Python
的后臺線程呢?我們可能嘗試解決這個問題,卻發現線程是殺不死的。而本文中將展示,在Python
中用于終止線程的兩個方式。
1. 線程無法結束
A Threaded Example
-
下面是一個簡單的,多線程的示例代碼。
import random
import threading
import time
def bg_thread():
for i in range(1, 30):
print(f'{i} of 30 iterations...')
time.sleep(random.random()) # do some work...
print(f'{i} iterations completed before exiting.')
th = threading.Thread(target=bg_thread)
th.start()
th.join()
-
使用下面命令來運行程序,在下面的程序運行中,當跑到第
7
次迭代時,按下Ctrl-C
來中斷程序,發現后臺運行的程序并沒有終止掉。而在第13
次迭代時,再次按下Ctrl-C
來中斷程序,發現程序真的退出了。
$ python thread.py
1 of 30 iterations...
2 of 30 iterations...
3 of 30 iterations...
4 of 30 iterations...
5 of 30 iterations...
6 of 30 iterations...
7 of 30 iterations...
^CTraceback (most recent call last):
File "thread.py", line 14, in
th.join()
File "/Users/mgrinberg/.pyenv/versions/3.8.6/lib/python3.8/threading.py", line 1011, in join
self._wait_for_tstate_lock()
File "/Users/mgrinberg/.pyenv/versions/3.8.6/lib/python3.8/threading.py", line 1027, in _wait_for_tstate_lock
elif lock.acquire(block, timeout):
KeyboardInterrupt
8 of 30 iterations...
9 of 30 iterations...
10 of 30 iterations...
11 of 30 iterations...
12 of 30 iterations...
13 of 30 iterations...
^CException ignored in:
Traceback (most recent call last):
File "/Users/mgrinberg/.pyenv/versions/3.8.6/lib/python3.8/threading.py", line 1388, in _shutdown
lock.acquire()
KeyboardInterrupt:
-
這很奇怪,不是嗎?究其原因是,Python 有一些邏輯是會在進程退出前運行的,專門用來等待任何沒有被配置為守護線程的后臺線程結束,然后再把控制權真正交給操作系統。因此,該進程在其主線程運行時收到到了中斷信號,并準備退出。首先,它需要等待后臺線程運行結束。但是,這個線程對中斷一無所知,這個線程只知道它需要在運行結束前完成 30次迭代。
-
Python 在退出過程中使用的等待機制有一個規定,當收到第二個中斷信號時,就會中止。這就是為什么第二個 Ctrl-C 會立即結束進程。所以我們看到了,線程是不能被殺死!在下面的章節中,將向展示 Python 中的兩個方式,來使線程及時結束。
2. 使用守護進程
Daemon Threads
-
在上面提到過,在
Python
退出之前,它會等待任何非守護線程的線程。而守護線程就是,一個不會阻止Python
解釋器退出的線程。 -
如何使一個線程成為一個守護線程?所有的線程對象都有一個
daemon
屬性,可以在啟動線程之前將這個屬性設置為True
,然后該線程就會被視為一個守護線程。下面是上面的示例應用程序,修改后守護線程版本:
import random
import threading
import time
def bg_thread():
for i in range(1, 30):
print(f'{i} of 30 iterations...')
time.sleep(random.random()) # do some work...
print(f'{i} iterations completed before exiting.')
th = threading.Thread(target=bg_thread)
th.daemon = True
th.start()
th.join()
-
再次運行它,并嘗試中斷它,發現第一個執行
Ctrl-C
后進程立即就退出了。
~ $ python x.py
1 of 30 iterations...
2 of 30 iterations...
3 of 30 iterations...
4 of 30 iterations...
5 of 30 iterations...
6 of 30 iterations...
^CTraceback (most recent call last):
File "thread.py", line 15, in
th.join()
File "/Users/mgrinberg/.pyenv/versions/3.8.6/lib/python3.8/threading.py", line 1011, in join
self._wait_for_tstate_lock()
File "/Users/mgrinberg/.pyenv/versions/3.8.6/lib/python3.8/threading.py", line 1027, in _wait_for_tstate_lock
elif lock.acquire(block, timeout):
KeyboardInterrupt
-
那么這個線程會發生什么呢?線程繼續運行,就像什么都沒發生一樣,直到
Python
進程終止并返回到操作系統。這時,線程就不存在了。你可能認為這實際上是一種殺死線程的方法,但要考慮到以這種方式殺死線程,你必須同時殺死進程。
3. 使用事件對象
Python Events
-
使用守護線程,是一種避免在多線程程序中處理意外中斷的簡單方法,但這是一種只在進程退出的特殊情況下才有效的技巧。不幸的是,有些時候,一個應用程序可能想結束一個線程而不必殺死自己。另外,有些線程可能需要在退出前執行清理工作,而守護線程則不允許這樣操作。
-
那么,還有什么其他選擇呢?既然不可能強制線程結束,那么唯一的選擇就是給它添加邏輯,讓它在被要求退出時自愿退出。有多種方法都可以解決上述問題,但我特別喜歡的一種方法,就是使用一個
Event
對象。
Event
類是由Python
標準庫的線程模塊提供,你可以通過實例化類來創建一個事件對象,就像下面這個樣子:
exit_event = threading.Event()
-
Event
對象可以處于兩種狀態之一:set
或not set
。當我們實例化創建之后,默認事件并沒有被設置。-
若要將事件狀態更改為
set
,則可以調用set()
方法; -
要查明是否設置了事件,使用
is_set()
方法,設置了則返回True
; -
還可以使用
wait()
方法等待事件,等待操作阻塞直到設置事件(可以設置超時)
-
-
其核心思路,就是在線程需要退出的時候設置事件。然后,線程需要經常地檢查事件的狀態(通常是在循環中),并在發現事件已經設置時處理自己的終止。對于上面顯示的示例,一個好的解決方案是添加一個捕獲
Ctrl-C
中斷的信號處理程序,而不是突然退出,只需設置事件并讓線程優雅地結束。
import random
import signal
import threading
import time
exit_event = threading.Event()
def bg_thread():
for i in range(1, 30):
print(f'{i} of 30 iterations...')
time.sleep(random.random()) # do some work...
if exit_event.is_set():
break
print(f'{i} iterations completed before exiting.')
def signal_handler(signum, frame):
exit_event.set()
signal.signal(signal.SIGINT, signal_handler)
th = threading.Thread(target=bg_thread)
th.start()
th.join()
-
如果你嘗試中斷這個版本的應用程序,一切看起來都會更好:
$ python thread.py
1 of 30 iterations...
2 of 30 iterations...
3 of 30 iterations...
4 of 30 iterations...
5 of 30 iterations...
6 of 30 iterations...
7 of 30 iterations...
^C7 iterations completed before exiting.
-
需要注意的是,中斷是如何被優雅地處理的,以及線程能夠運行在循環之后出現的代碼。如果當線程需要在退出之前,關閉文件句柄或數據庫連接時,這種方式就非常有用了。其能夠在線程退出之前,運行清理代碼有時是必要的,以避免資源泄漏。我在上面提到過,
event
對象也是可以等待的:
for i in range(1, 30):
print(f'{i} of 30 iterations...')
time.sleep(random.random())
if exit_event.is_set():
break
-
在每個迭代中,都有一個對
time.sleep()
的調用,這將阻塞線程。如果在線程sleep
時設置了退出事件,那么它就不能檢查事件的狀態,因此在線程能夠退出之前會有一個小的延遲。在這種情況下,如果有sleep
,使用wait()
方法將sleep
與event
對象的檢查結合起來會更有效:
for i in range(1, 30):
print(f'{i} of 30 iterations...')
if exit_event.wait(timeout=random.random()):
break
-
這個解決方案有效地為提供了一個可中斷的
sleep
,因為在線程停留在wait()
調用的中間時設置了事件,那么等待將立即返回。
4. 總結陳述說明
Conclusion
-
你知道
Python
中的event
對象嗎?它們是比較簡單的同步原語之一,不僅可以用作退出信號,而且在線程需要等待某些外部條件發生的許多其他情況下也可以使用。
原文鏈接:https://www.escapelife.site/posts/558f583c.html
-
python
+關注
關注
56文章
4807瀏覽量
85040 -
線程
+關注
關注
0文章
505瀏覽量
19758
原文標題:如何殺死一個Python線程
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論