社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

Python 线程为什么要搞个 setDaemon ?

Python中文社区 • 5 年前 • 635 次点击  

投稿自 网易游戏运维平台 公众号: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-1started daemon 123145439883264)>
3<Thread(Thread-2started daemon 123145444089856)>
4(直接退出了)

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

突如其来的 daemon

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




    
 1#include 
2#include 
3#include 
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>]
 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 可以引出很多本质内容的探索机会,比如线程的创建过程,管理流程等。

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

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

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/52553
 
635 次点击