首頁  >  文章  >  後端開發  >  非強制終止python執行緒的方法介紹

非強制終止python執行緒的方法介紹

Y2J
Y2J原創
2017-05-06 14:51:231663瀏覽

本文給大家分享的是走著的一些強制殺掉python線程經驗教訓,如果你使用強製手段幹掉線程,那麼很大幾率出現意想不到的bug。  請記住一點,鎖定資源不會因為執行緒退出而釋放鎖定資源 !

前言:

    不要試圖用強制方法殺死一個python線程,這從服務設計就存在不合理性。 多線程本用來任務的協作並發,如果你使用強製手段幹掉線程,那麼很大幾率出現意想不到的bug。  請記住一點,鎖定資源不會因為執行緒退出而釋放鎖定資源 !

我們可以舉出兩個常見的例子:

1. 有個A線程拿到了鎖,因為他是被強制幹掉的,沒能及時的release()釋放鎖資源,那麼導致所有的執行緒獲取資源是都被阻塞下去,這就是典型的死鎖場景。

2.在常見的生產消費者的場景下,消費者從任務隊列獲取任務,但是被幹掉後沒有把正在做的任務丟回隊列中,那麼這就造成了數據丟失。

下面是java和python終止執行緒的方法:

java有三種方法可以讓終止執行緒:

1. 使用退出標誌,使執行緒正常退出,也就是當run方法完成後執行緒終止。
2. 使用stop方法強行終止執行緒(不建議使用,因為stop和suspend、resume一樣,也可能發生不可預料的結果)。
3. 使用interrupt方法中斷執行緒。

python可以有兩種方法:

1. 退出標記
2. 使用ctypes強行殺掉執行緒

##不管是python還是java環境下,理想的停止退出線程方法是讓線程自個自殺,所謂的線程自殺就是你給他一個標誌位,他退出線程。

下面我們會採用多種方法來測試 停止python執行緒的異常情況。我們檢視一個行程所有的執行緒,  行程是用過掌控資源,執行緒是用作調度單元,行程要被調度執行必須要有一個執行緒,預設的執行緒和行程的pid一樣的。

ps -mp 31449 -o THREAD,tid
 
USER   %CPU PRI SCNT WCHAN USER SYSTEM  TID
root   0.0  -  - -     -   -   -
root   0.0 19  - poll_s  -   - 31449
root   0.0 19  - poll_s  -   - 31450

取得到了流程所有的執行緒後,透過strace得知 31450 是需要我們kill的執行緒id,當我們kill的時候,會出現整個行程都崩潰的狀況。 在多執行緒環境下,產生的訊號是傳遞給整個行程的,一般而言,所有執行緒都有機會收到這個訊號,行程在收到訊號的的執行緒上下文執行訊號處理

函數,具體是哪個執行緒執行的難以獲知。也就是說,訊號會隨機發個該行程的一個執行緒。

strace -p <span style="font-size:14px;line-height:21px;">31450</span> Process <span style="font-size:14px;line-height:21px;">31450</span> attached - interrupt to quit
select(0, NULL, NULL, NULL, {0, 320326}) = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})   = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})   = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})   = ? ERESTARTNOHAND (To be restarted)
--- SIGTERM (Terminated) @ 0 (0) ---
Process <span style="font-size:14px;line-height:21px;">31450</span> detached

上面出現的問題其實跟pthread的說明是一致的。當我們在python程式碼裡加入signal 訊號處理函數後,

回呼函數可以防止整個行程的退出,那麼問題來了,透過訊號函數不能辨識你要幹掉哪一個線程,也就是說,不能精準的幹掉某個線程。你雖然把訊號發給31450線程id,但是訊號受理人是所屬進程的任何一個,另外傳給訊號處理函數的參數只有訊號數和訊號stack而已,可有可無的。

加上了訊號處理後,不會退出進程

select(0, NULL, NULL, NULL, {1, 0})   = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})   = ? ERESTARTNOHAND (To be restarted)
--- SIGTERM (Terminated) @ 0 (0) ---
rt_sigreturn(0xffffffff)        = -1 EINTR (Interrupted system call)
select(0, NULL, NULL, NULL, {1, 0})   = 0 (Timeout)
select(0, NULL, NULL, NULL, {1, 0})   = 0 (Timeout)

如果想從外部通知殺掉某個線程,那麼可以建構使用rpc服務,或是別的方式通信,signal訊號不可以,因為無法無法傳遞更多的訊息。

python的線程不是模擬的,是真實的內核線程,內核呼叫pthread方法,但Python上層沒有提供關閉線程的方法,這就需要我們自己把握了。強烈建議使用 event 或 自訂標誌位的方法, 如果非要強制殺掉線程,那麼可以用python ctypes PyThreadState

SetAsyncExc 方法強制退出,這樣對於運行的python服務沒有什麼影響。

該函數的實作原理比較簡單,其實也是在python虛擬機器裡做個標示位,然後由虛擬機器運行一個異常來取消線程,虛擬機會幫你做好try

cache。 記得不要在外部殺掉python的某個線程,雖然你能透過ctypes找到線程id,但你直接kill會幹掉整個行程的。

下面的程式碼是 用ctypes 殺掉線程的範例,不建議使用,因為太粗暴了.

import ctypes
 
def terminate_thread(thread):
  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:
    ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None)
    raise SystemError("PyThreadState_SetAsyncExc failed")

咱們簡單look一下PyThreadState原始程式碼,總而言之觸發線程的異常模式。 有興趣的人可以閱讀 python pystate.c 的設計,配合著youtube的一些

影片分享。

 int
PyThreadState_SetAsyncExc(long id, PyObject *exc) {
  PyInterpreterState *interp = GET_INTERP_STATE();
  ...
  HEAD_LOCK();
  for (p = interp->tstate_head; p != NULL; p = p->next) {
    if (p->thread_id == id) {
      从链表里找到线程的id,避免死锁,我们需要释放head_mutex。
      PyObject *old_exc = p->async_exc;
      Py_XINCREF(exc); #增加该对象的引用数
      p->async_exc = exc; # 更为exc模式
      HEAD_UNLOCK();
      Py_XDECREF(old_exc); # 因为要取消,当然也就递减引用
      ...
      return 1; #销毁线程成功
    }
  }
  HEAD_UNLOCK();
  return 0;
}

原生posix pthread 可以使用 ptread_cancel(tid) 在主线程中结束子线程。但是 Python 的线程库不支持这样做,理由是我们不应该强制地结束一个线程,这样会带来很多隐患,应该让该线程自己结束自己。所以在 Python 中,推荐的方法是在子线程中循环判断一个标志位,在主线程中改变该标志位,子线程读到标志位改变,就结束自己。

类似这个逻辑:

def consumer_threading():
 t1_stop= threading.Event()
 t1 = threading.Thread(target=thread1, args=(1, t1_stop))
 
 t2_stop = threading.Event()
 t2 = threading.Thread(target=thread2, args=(2, t2_stop))
 
 time.sleep(duration)
 #stop the thread2
 t2_stop.set()
 
def thread1(arg1, stop_event):
 while(not stop_event.is_set()):
   #similar to time.sleep()
   stop_event.wait(time)
   pass
 
 
def thread2(arg1, stop_event):
 while(not stop_event.is_set()):
   stop_event.wait(time)
   pass

简单的总结,虽然我们可以用ctypes里的pystats来控制线程,但这种粗暴中断线程的方法是不合理的。 请选用 自杀模式 !如果你的线程正在发生io阻塞,而不能判断事件怎么办? 你的程序需要做优化了,最少在网络io层需要有主动的timeout,避免一直的阻塞下去。

【相关推荐】

1. Python免费视频教程

2. Python基础入门教程

3. Python面向对象视频教程

以上是非強制終止python執行緒的方法介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn