Python中關於Thread的一點小知識

我就看看發表於2019-02-26

最近在實現了一個對sqlite3進行簡單封裝的非同步庫aiosqlite,讓其支援非同步的方式呼叫。因為是python2.7,標準庫中沒有原生的類似asyncio的模組,所以依賴第三方tornado庫。

由於sqlite3本身查詢資料檔案的操作是阻塞的,要想實現非同步呼叫,就不得不通過多執行緒的方式,在執行查詢語句的時候通過多執行緒操作,從而達到偽非同步。

使用多執行緒的過程中,剛好跟同事聊了幾句關於多執行緒的問題,儘管可能是一些基礎的知識,但是平時由於沒怎麼考慮過這塊的問題,所以覺得記錄一下也好。

以下程式碼皆基於python2.7版本

Python中當開啟的執行緒任務結束後,執行緒是否被銷燬了?

關於這個問題我理所當然的認為是銷燬了,但是同事的看法是:執行緒執行完成任務後退出,但實際並沒有銷燬,python本身也沒有銷燬執行緒的功能,想銷燬執行緒的話要通過作業系統本身提供的介面。

通常我們在使用Thread時,都是start()開始執行緒任務,最終join()結束執行緒,所以看了下cpythonthreading.Thread的原始碼,關於join()方法的說明中,也並未明確指出執行緒銷燬的問題。

最終還是得通過實踐出真知。在CentOS 7 x64系統中寫了點測試程式碼簡單驗證一下。關於程式的執行緒數可以通過cat /proc/<pid>/status|grep Thread檢視。

import time
from threading import Thread

def t1():
    print(`Thread 1 start`)
    time.sleep(10)
    print(`Thread 1 done`)

def t2():
    print(`Thread 2 start`)
    time.sleep(30)
    print(`Thread 2 done`)

t1 = Thread(target=t1, daemon=True)
t2 = Thread(target=t2, daemon=True)

for task in (t1, t2):
    task.start()

for task in (t1, t2):
    task.join()
複製程式碼

開始執行後檢視到的Thread數是3,當Thread 1結束後再次檢視發現Thread 2數變為2。可見,執行緒任務結束後,執行緒銷燬了的。

執行緒任務中,如果其中一個執行緒阻塞,其他的執行緒是否還正常執行?

關於這個問題,就顯得我有些愚蠢了。由於滿腦子想的都是GIL,同一時間只可能有一個執行緒在跑,那如果這個執行緒阻塞了,其他的執行緒肯定也跑不下去了,所以就認為一個執行緒阻塞,其他的執行緒肯定也阻塞了。同事的看法則是,肯定不會阻塞,不然還叫什麼多執行緒。

實際通過demo測試後發現,一個執行緒阻塞並不會對其他執行緒造成影響。由於對GIL一知半解,所以造成這種錯誤認知。看了下GIL的資料後瞭解到,Python的多執行緒是呼叫系統多執行緒介面,GIL只是一把全域性鎖,一個執行緒執行時獲取到GIL後,執行過程中如果遇到IO阻塞,會釋放掉GIL,這樣輪到其他的執行緒執行。所以,不會存在一個執行緒阻塞,其他執行緒也跟著阻塞的問題。

這真是個低階的錯誤。。。

執行多執行緒任務時,如果其中一個執行緒中執行了sys.exit()整個程式是否會退出?

同事的看法是會退出,我和另一個同事則不太敢肯定。demo跑跑。

import sys
import time
from threading import Thread

def t1():
    print(`Thread 1 start`)
    sys.exit()
    print(`Thread 1 done`)

def t2():
    k = 10
    if k:
        print(`Thread 2 is running`)
        time.sleep(3)
        k -= 1

t1 = Thread(target=t1, daemon=True)
t2 = Thread(target=t2, daemon=True)

for task in (t1, t2):
    task.start()

for task in (t1, t2):
    task.join()
複製程式碼

結果是,直到t2執行結束後程式才會退出,t1中的sys.exit()並不會造成整個程式的退出。

看原始碼sysmodule.c

static PyObject *
sys_exit(PyObject *self, PyObject *args)
{
    PyObject *exit_code = 0;
    if (!PyArg_UnpackTuple(args, "exit", 0, 1, &exit_code))
        return NULL;
    /* Raise SystemExit so callers may catch it or clean up. */
    PyErr_SetObject(PyExc_SystemExit, exit_code);
    return NULL;
}
複製程式碼

可以看到,返回值總是NULL,但在exit_code不為0時,會set一個PyExc_SystemExit。全域性搜尋一下PyExc_SystemExit
_threadmodule.c中可以找到

...
PyDoc_STRVAR(start_new_doc,
"start_new_thread(function, args[, kwargs])

(start_new() is an obsolete synonym)



Start a new thread and return its identifier.  The thread will call the

function with positional arguments from the tuple args and keyword arguments

taken from the optional dictionary kwargs.  The thread exits when the

function returns; the return value is ignored.  The thread will also exit

when the function raises an unhandled exception; a stack trace will be

printed unless the exception is SystemExit.
");

static PyObject *
thread_PyThread_exit_thread(PyObject *self)
{
    PyErr_SetNone(PyExc_SystemExit);
    return NULL;
}
複製程式碼

其實執行緒任務正常退出也會set一個PyExc_SystemExit,所以線上程中sys.exit()並不會讓整個程式退出。

以上僅為個人見解,如有認知錯誤,歡迎指正,謝謝。

參考:

Python的GIL是什麼

python GIL

Python/sysmodule.c

Modules/_threadmodule.c

相關文章