引
靈感來源依舊是爬蟲框架項(xiàng)目pycrawler庵楷,爬蟲作為子線程運(yùn)行時(shí)不受鍵盤中斷信號(hào)影響罢艾,Ctrl-C無法終止整個(gè)爬蟲運(yùn)行。另外的一個(gè)場景是多線程壓力測試尽纽,需要提前終止的情況下咐蚯,Ctrl-C依舊不能終止整個(gè)程序。除了簡單粗暴的使用kill命令強(qiáng)行終止之外弄贿,本文將給出一個(gè)簡單可行的解決方案春锋。
值得注意的一點(diǎn)是,Python2差凹、3兩個(gè)版本在測試中的表現(xiàn)并不一致期奔,所以使用兩個(gè)版本分別進(jìn)行測試。
博客原文
測試環(huán)境
- Python2 2.7.9
- Python3 3.4.2
- Mac OS X Yosemite 10.10.3
子線程類
import time
from threading import Thread
class CountDown(Thread):
def __init__(self):
super(CountDown, self).__init__()
def run(self):
num = 100
print('slave start')
for i in range(10, 0, -1):
print('Num: {0}'.format(num/i))
time.sleep(1)
print('slave end')
失敗情況一
主線程代碼
if __name__ == '__main__':
print('main start')
CountDown().start()
print('main end')
使用Python2進(jìn)行測試危尿,在運(yùn)行結(jié)束之前手動(dòng)終止:
main start
slave start
main end
Num: 10
Num: 11
^CNum: 12
Num: 14
Num: 16
Num: 20
Num: 25
Num: 33
Num: 50
Num: 100
slave end
Exception KeyboardInterrupt in <module 'threading' from '/usr/local/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.pyc'> ignored
可以看到呐萌,運(yùn)行到第三行時(shí)手動(dòng)終止,主線程已經(jīng)提前結(jié)束谊娇,子線程繼續(xù)執(zhí)行直到結(jié)束肺孤,然后拋出未捕獲異常,值得注意的是邮绿,此時(shí)沒有Traceback
輸出渠旁。
接下來使用Python3測試同樣的代碼:
main start
slave start
main end
Num: 10.0
Num: 11.11111111111111
Num: 12.5
^CException ignored in: <module 'threading' from '/usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/threading.py'>
Traceback (most recent call last):
File "/usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/threading.py", line 1294, in _shutdown
t.join()
File "/usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/threading.py", line 1060, in join
self._wait_for_tstate_lock()
File "/usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/threading.py", line 1076, in _wait_for_tstate_lock
elif lock.acquire(block, timeout):
KeyboardInterrupt
有趣的事情發(fā)生了攀例,主線程依舊提前退出船逮,子線程在手動(dòng)終止后也被強(qiáng)行終止,雖然打印了Traceback
信息粤铭,但和上例一樣依舊是Exception ignored
挖胃。
失敗情況二
主線程代碼,現(xiàn)在調(diào)用join()方法使主線程等待子線程完成:
if __name__ == '__main__':
print('main start')
td = CountDown()
td.start()
td.join()
print('main end')
同上,使用Python2進(jìn)行測試:
main start
slave start
Num: 10
Num: 11
Num: 12
^CNum: 14
Num: 16
Num: 20
Num: 25
Num: 33
Num: 50
Num: 100
slave end
Traceback (most recent call last):
File "multithread.py", line 23, in <module>
td.join()
File "/usr/local/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 949, in join
self.__block.wait()
File "/usr/local/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 340, in wait
waiter.acquire()
KeyboardInterrupt
可以看出酱鸭,主線程調(diào)用join()方法之后wait在一個(gè)鎖上,等待子線程退出。子線程退出后瓦灶,主線程獲得鎖并響應(yīng)中斷信號(hào)奶陈,拋出異常并打印信息,main end
一行并沒有被打印蔚舀。
然后使用Python3進(jìn)行測試:
main start
slave start
Num: 10.0
Num: 11.11111111111111
Num: 12.5
^CTraceback (most recent call last):
File "multithread.py", line 23, in <module>
td.join()
File "/usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/threading.py", line 1060, in join
self._wait_for_tstate_lock()
File "/usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/threading.py", line 1076, in _wait_for_tstate_lock
elif lock.acquire(block, timeout):
KeyboardInterrupt
Num: 14.285714285714286
Num: 16.666666666666668
Num: 20.0
Num: 25.0
Num: 33.333333333333336
Num: 50.0
Num: 100.0
slave end
神奇的事情再次發(fā)生了饵沧,主線程等待子線程過程中響應(yīng)了中斷信號(hào),打印信息后退出赌躺,而此時(shí)子線程沒有受影響狼牺,繼續(xù)執(zhí)行直到結(jié)束。
思考
從Python3的Traceback
信息可以看出礼患,即使沒有顯式調(diào)用join()方法是钥,主線程執(zhí)行完成后依然會(huì)自動(dòng)調(diào)用一個(gè)join()邏輯(line 1294, in _shutdown
),而且個(gè)方法對(duì)子線程的影響并不一致:顯式調(diào)用時(shí)子線程不受影響繼續(xù)執(zhí)行缅叠;而自動(dòng)調(diào)用時(shí)悄泥,子線程隨主線程一起退出。
根據(jù)Traceback
信息查看Python3源代碼:
def _shutdown():
# Obscure: other threads may be waiting to join _main_thread. That's
# dubious, but some code does it. We can't wait for C code to release
# the main thread's tstate_lock - that won't happen until the interpreter
# is nearly dead. So we release it here. Note that just calling _stop()
# isn't enough: other threads may already be waiting on _tstate_lock.
tlock = _main_thread._tstate_lock
# The main thread isn't finished yet, so its thread state lock can't have
# been released.
assert tlock is not None
assert tlock.locked()
tlock.release()
_main_thread._stop()
t = _pickSomeNonDaemonThread()
while t:
t.join()
t = _pickSomeNonDaemonThread()
_main_thread._delete()
def _pickSomeNonDaemonThread():
for t in enumerate():
if not t.daemon and t.is_alive():
return t
return None
從源代碼可以看出痪署,主線程調(diào)用了_stop()
方法码泞,然后循環(huán)等待所有非daemon進(jìn)程執(zhí)行結(jié)束,最終調(diào)用_delete()
方法結(jié)束運(yùn)行狼犯。所以主線程雖然執(zhí)行完了所有的代碼余寥,但是其實(shí)并未真正退出,而是等待所有非daemon子線程全部執(zhí)行完畢后才釋放資源退出程序(所有daemon線程也隨之被銷毀)悯森,這個(gè)過程中宋舷,主線程僅僅占有資源但并沒有執(zhí)行邏輯(這里我的理解是,不會(huì)響應(yīng)中斷信號(hào))瓢姻。
所以祝蝠,得出結(jié)論:
- 沒有調(diào)用
join()
的情況下,主線程退出執(zhí)行邏輯但沒有釋放資源幻碱,且不響應(yīng)中斷信號(hào)绎狭,此時(shí)中斷信號(hào)由子線程響應(yīng),于是在失敗情況一中褥傍,程序成功被終止儡嘶。 - 顯式調(diào)用
join()
的情況下,主線程沒有執(zhí)行后續(xù)代碼恍风,而是等待子線程釋放鎖蹦狂,因此可以響應(yīng)中斷信號(hào)誓篱,于是在失敗情況二中,主線程響應(yīng)中斷信號(hào)并執(zhí)行退出邏輯(進(jìn)入上一種情況)凯楔,子線程并未受影響窜骄,執(zhí)行完畢后程序退出。
但是對(duì)于Python2和Python3之間的差別摆屯,我現(xiàn)在依舊沒有想明白邻遏,初步的想法是2和3對(duì)于異常的處理邏輯(或者說順序)不一致,導(dǎo)致2中所有的異常都在主線程真正退出時(shí)才被捕獲虐骑。打算去知乎問一下党远,之后會(huì)補(bǔ)上問題鏈接
解決方案
說了這么多終于到解決方案了。思路是:設(shè)置子線程為daemon線程富弦,啟動(dòng)子線程后主線程調(diào)用isAlive()
方法手動(dòng)模擬join過程沟娱。
代碼如下:
if __name__ == '__main__':
print('main start')
td = CountDown()
td.setDaemon(True)
td.start()
try:
while td.isAlive():
pass
except KeyboardInterrupt:
print('stopped by keyboard')
print('main end')
測試輸出(Python2、3執(zhí)行結(jié)果一致):
main start
slave start
Num: 10
Num: 11
Num: 12
Num: 14
Num: 16
^Cstopped by keyboard
main end
此方案另一優(yōu)點(diǎn)是主線程可以繼續(xù)執(zhí)行之后的善后邏輯腕柜。
結(jié)
- 感覺這種解決方案算是非主流小技巧了济似,我想了好久才想出來,具體應(yīng)用中實(shí)不實(shí)用還不知道盏缤,畢竟現(xiàn)在接觸的項(xiàng)目都太小了砰蠢。
- 從這個(gè)例子看出,Python2和Python3之間除了官網(wǎng)提到的關(guān)鍵區(qū)別唉铜,還有很多細(xì)微的差別台舱,這些差別對(duì)某些特定程序可能會(huì)產(chǎn)生一定影響,只能慢慢摸索著發(fā)現(xiàn)了潭流。
- 這篇文章算是多線程tricky技巧第一篇竞惋。多線程程序中另一個(gè)重要問題是主線程如何捕獲子線程產(chǎn)生的異常,我構(gòu)思了一個(gè)方案灰嫉,有時(shí)間測試一下拆宛。
- 如果有哪些不對(duì)的地方或者有更好的解決方案,歡迎討論讼撒。
- 謝謝~