Python 线程为什么要搞个 setDaemon ?

picture.image

投稿自 网易游戏运维平台

公众号: neteasegameops

作者:渣渣飞,长年在票圈深夜放毒,是网易游戏高级运维工程师,对代码性能及系统原理饶有兴趣,三人行,必有我师。现负责监控相关业务开发 。

前言

使用 Python 都不会错过线程这个知识,但是每次谈到线程,大家都下意识说 GIL 全局锁,

但其实除了这个老生常谈的话题,还有很多有价值的东西可以探索的,譬如:setDaemon()

线程的使用 与 存在的问题

我们会写这样的代码来启动多线程:


          
 1import time  
 2import threading  
 3  
 4def test():  
 5    while True:  
 6        print threading.currentThread()  
 7        time.sleep(1)  
 8  
 9if __name__ == '\_\_main\_\_':  
10    t1 = threading.Thread(target=test)  
11    t2 = threading.Thread(target=test)  
12    t1.start()  
13    t2.start()  

      

输出:


          
 1^C<Thread(Thread-2, started 123145414086656)>  
 2<Thread(Thread-1, started 123145409880064)>  
 3^C^C^C^C^C^C<Thread(Thread-2, started 123145414086656)>    # ctrl-c 多次都无法中断  
 4 <Thread(Thread-1, started 123145409880064)>  
 5^C<Thread(Thread-1, started 123145409880064)>  
 6 <Thread(Thread-2, started 123145414086656)>  
 7<Thread(Thread-1, started 123145409880064)>  
 8 <Thread(Thread-2, started 123145414086656)>  
 9<Thread(Thread-2, started 123145414086656)><Thread(Thread-1, started 123145409880064)>  
10...(两个线程竞相打印)  

      

通过 Threading 我们可以很简单的实现并发的需求,但是同时也给我们带来了一个大难题: 怎么退出呢?

在上面的程序运行中,我已经尝试按了多次的 ctrl-c ,都无法中断这程序工作的热情!最后是迫不得已用 kill 才结束。

那么怎样才能可以避免这种问题呢?或者说,怎样才能在主线程退出的时候,子线程也自动退出呢?

守护线程

有过相似经验的老司机肯定就知道,setDaemon() 将线程搞成 守护线程 不就得了呗:


          
 1import time  
 2import threading  
 3  
 4def test():  
 5    while True:  
 6        print threading.currentThread()  
 7        time.sleep(1)  
 8  
 9if __name__ == '\_\_main\_\_':  
10    t1 = threading.Thread(target=test)  
11    t1.setDaemon(True)  
12    t1.start()  
13  
14    t2 = threading.Thread(target=test)  
15    t2.setDaemon(True)  
16    t2.start()  

      

输出:


          
1python2.7 1.py  
2<Thread(Thread-1, started daemon 123145439883264)>  
3<Thread(Thread-2, started daemon 123145444089856)>  
4(直接退出了)  

      

直接退出?理所当然,因为主线程已经执行完了,确实是已经结束了,正因为设置了守护线程,所以这时候子线程也一并退出了。

突如其来的 daemon

那么问题来了,我们以前学 C 语言的时候,好像不用 Daemon 也可以啊,比如这个:


          
 1#include <stdio.h>  
 2#include <sys/syscall.h>  
 3#include <pthread.h>  
 4  
 5void *test(void *args)  
 6{  
 7    while (1)  
 8    {  
 9        printf("ThreadID: %d\n", syscall(SYS_gettid));  
10        sleep(1);  
11    }  
12}  
13  
14int main()  
15{  
16    pthread\_t t1 ;  
17    int ret = pthread_create(&t1, NULL, test, NULL);  
18    if (ret != 0)  
19    {  
20        printf("Thread create failed\n");  
21    }  
22  
23    // 避免直接退出  
24    sleep(2);  
25    printf("Main run..\n");  
26}  

      

输出:


          
1# gcc -lpthread test\_pytha.out & ./a  
2ThreadID: 31233  
3ThreadID: 31233  
4Main run.. (毫不犹豫退出了)  

      

既然 Python 也是用 C 写的,为什么 Python 多线程退出需要 setDaemon ???

想要解决这个问题,我们怕不是要从主线程退出的一刻开始讲起,从前….

反藤摸瓜

Python 解析器在结束的时候,会调用 wait_for_thread_shutdown 来做个例行清理:


          
 1// python2.7/python/pythonrun.c  
 2  
 3static void  
 4wait\_for\_thread\_shutdown(void)  
 5{  
 6#ifdef WITH\_THREAD  
 7    PyObject *result;  
 8    PyThreadState *tstate = PyThreadState_GET();  
 9    PyObject *threading = PyMapping_GetItemString(tstate->interp->modules,  
10                                                  "threading");  
11    if (threading == NULL) {  
12        /* threading not imported */  
13        PyErr_Clear();  
14        return;  
15    }  
16    result = PyObject_CallMethod(threading, "\_shutdown", "");  
17    if (result == NULL)  
18        PyErr_WriteUnraisable(threading);  
19    else  
20        Py_DECREF(result);  
21    Py_DECREF(threading);  
22#endif  
23}  

      

我们看到 #ifdef WITH_THREAD 就大概猜到对于是否多线程,这个函数是运行了不同的逻辑的

很明显,我们上面的脚本,就是命中了这个线程逻辑,所以它会动态 import threading 模块 ,然后执行 _shutdown 函数。

这个函数的内容,我们可以从 threading 模块看到:


          
 1# /usr/lib/python2.7/threading.py  
 2  
 3_shutdown = _MainThread()._exitfunc  
 4  
 5class \_MainThread(Thread):  
 6  
 7    def \_\_init\_\_(self):  
 8        Thread.__init_\_(self, name="MainThread")  
 9        self._Thread__started.set()  
10        self._set_ident()  
11        with \_active\_limbo\_lock:  
12            _active[_get_ident()] = self  
13  
14    def \_set\_daemon(self):  
15        return False  
16  
17    def \_exitfunc(self):  
18        self._Thread__stop()  
19        t = _pickSomeNonDaemonThread()  
20        if t:  
21            if \_\_debug\_\_:  
22                self._note("%s: waiting for other threads", self)  
23        while t:  
24            t.join()  
25            t = _pickSomeNonDaemonThread()  
26        if \_\_debug\_\_:  
27            self._note("%s: exiting", self)  
28        self._Thread__delete()  
29  
30def \_pickSomeNonDaemonThread():  
31    for t in enumerate():  
32        if not t.daemon and t.is_alive():  
33            return t  
34    return None  

      

_shutdown 实际上也就是 _MainThread()._exitfunc 的内容,主要是将 enumerate() 返回的所有结果,全部 join() 回收

enumerate() 是什么?

这个平时我们也会使用,就是当前进程的所有 符合条件 的 Python线程对象:


          
1>>> print threading.enumerate()  
2[<\_MainThread(MainThread, started 140691994822400)>]  

      

          
 1# /usr/lib/python2.7/threading.py  
 2  
 3def enumerate():  
 4    """Return a list of all Thread objects currently alive.  
 5  
 6    The list includes daemonic threads, dummy thread objects created by  
 7    current\_thread(), and the main thread. It excludes terminated threads and  
 8    threads that have not yet been started.  
 9  
10    """  
11    with _active_limbo_lock:  
12        return _active.values() + _limbo.values()  

      

符合条件???符合什么条件??不着急,容我娓娓道来:

从起源谈存活条件

在 Python 的线程模型里面,虽然有 GIL 的干涉,但是线程却是实实在在的原生线程

Python 只是多加一层封装: t_bootstrap ,然后再在这层封装里面执行真正的处理函数。

threading 模块内,我们也能看到一个相似的:


          
 1# /usr/lib/python2.7/threading.py  
 2  
 3class Thread(\_Verbose):  
 4    def start(self):  
 5        ...省略  
 6        with _active_limbo_lock:  
 7            _limbo[self] = self             # 重点  
 8        try:  
 9            _start_new_thread(self.__bootstrap, ())  
10        except Exception:  
11            with _active_limbo_lock:  
12                del _limbo[self]            # 重点  
13            raise  
14        self.__started.wait()  
15  
16    def \_\_bootstrap(self):  
17        try:  
18            self.__bootstrap_inner()  
19        except:  
20            if self.__daemonic and _sys is None:  
21                return  
22            raise  
23  
24    def \_\_bootstrap\_inner(self):  
25        try:  
26            ...省略  
27            with _active_limbo_lock:  
28                _active[self.__ident] = self # 重点  
29                del _limbo[self]             # 重点  
30            ...省略  

      

在上面的一连串代码中,_limbo_active 的变化都已经标记了重点,我们可以得到下面的定义:


          
1    \_limbo : 就是调用了 start,但是还没来得及 _start_new_thread 的对象  
2    _active: 活生生的线程对象  

      

那么回到上文,当 _MainThread()._exitfunc 执行时,是会检查整个进程是否存在 _limbo + _active 的对象,

只要存在一个,就会调用 join() , 这个也就是堵塞的原因。

setDaemon 用处

无限期堵塞不行,自作聪明帮用户强杀线程也不是办法,那么怎么做才会比较优雅呢?

那就是提供一个途径,让用户来设置随进程退出的标记,那就是 setDaemon


          
 1class Thread():  
 2    ...省略  
 3    def setDaemon(self, daemonic):  
 4        self.daemon = daemonic  
 5  
 6    ...省略  
 7  
 8# 其实上面也贴了,这里再贴一次  
 9def \_pickSomeNonDaemonThread():  
10    for t in enumerate():  
11        if not t.daemon and t.is_alive():  
12            return t  
13    return None  

      

只要子线程,全部设置 setDaemon(True) , 那么主线程一准备退出,全都乖乖地由操作系统销毁回收。

之前一直很好奇,pthread 都没有 daemon 属性,为什么 Python 会有呢?

结果这玩意就是真的是仅作用于 Python 层(手动笑脸)

结语

区区一个 setDaemon 可以引出很多本质内容的探索机会,比如线程的创建过程,管理流程等。

这些都是很有意思的内容,我们应该大胆探索,不局限于使用~

picture.image

▼点击成为社区注册会员** 喜欢文章,点个** 在看 picture.image

0
0
0
0
评论
未登录
暂无评论