Python中關於Thread的一點小知識

最近在實現了一個對sqlite3進行簡單封裝的異步庫aiosqlite,讓其支持異步的方式調用。由於是python2.7,標準庫中沒有原生的相似asyncio的模塊,因此依賴第三方tornado庫。python

因爲sqlite3自己查詢數據文件的操做是阻塞的,要想實現異步調用,就不得不經過多線程的方式,在執行查詢語句的時候經過多線程操做,從而達到僞異步。ios

使用多線程的過程當中,恰好跟同事聊了幾句關於多線程的問題,儘管多是一些基礎的知識,可是平時因爲沒怎麼考慮過這塊的問題,因此以爲記錄一下也好。git

如下代碼皆基於python2.7版本github

Python中當開啓的線程任務結束後,線程是否被銷燬了?sql

關於這個問題我理所固然的認爲是銷燬了,可是同事的見解是:線程執行完成任務後退出,但實際並無銷燬,python自己也沒有銷燬線程的功能,想銷燬線程的話要經過操做系統自己提供的接口。bash

一般咱們在使用Thread時,都是start()開始線程任務,最終join()結束線程,因此看了下cpythonthreading.Thread的源碼,關於join()方法的說明中,也並未明確指出線程銷燬的問題。多線程

最終仍是得經過實踐出真知。在CentOS 7 x64系統中寫了點測試代碼簡單驗證一下。關於進程的線程數能夠經過cat /proc/<pid>/status|grep Thread查看。less

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。可見,線程任務結束後,線程銷燬了的。python2.7

線程任務中,若是其中一個線程阻塞,其餘的線程是否還正常運行?異步

關於這個問題,就顯得我有些愚蠢了。因爲滿腦子想的都是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])\n\ (start_new() is an obsolete synonym)\n\ \n\ Start a new thread and return its identifier. The thread will call the\n\ function with positional arguments from the tuple args and keyword arguments\n\ taken from the optional dictionary kwargs. The thread exits when the\n\ function returns; the return value is ignored. The thread will also exit\n\ when the function raises an unhandled exception; a stack trace will be\n\ printed unless the exception is SystemExit.\n");

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

相關文章
相關標籤/搜索