python 多线程中的 join 和 daemon

Posted 车子 chezi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python 多线程中的 join 和 daemon相关的知识,希望对你有一定的参考价值。

第一关:简单的 join()

import threading
import time


def run():
    time.sleep(2)
    print('当前线程的名字是: ', threading.current_thread().name)
    time.sleep(2)


if __name__ == '__main__':
    print('这是主线程:', threading.current_thread().name)
    thread_list = []
    
    for i in range(5):
        t = threading.Thread(target=run)
        thread_list.append(t)
        
    for t in thread_list:
        t.start()
        
    start_time = time.time()
    thread_list[0].join()
   
    print('一共用时:', time.time() - start_time)
    print('主线程结束!', threading.current_thread().name)

执行结果是:

这是主线程: MainThread
当前线程的名字是:  Thread-1
当前线程的名字是:  Thread-5当前线程的名字是:  Thread-4当前线程的名字是: 当前线程的名字是:  
 
Thread-2
Thread-3
一共用时: 4.014104843139648
主线程结束! MainThread

为了弄清楚 join() 的作用,我关注代码 23 行的用时。

第 23 行,主线程阻塞,直到 thread_list[0] 退出后才往下执行。

看打印结果第 7 行,用时 4s,这是好理解的,因为 thread_list[0] 大概用 4s。说明主线程确实阻塞了,一直阻塞到 thread_list[0] 执行完毕。

我们把代码修改一下,把 run() 函数的 sleep 时间增加。

def run():
    time.sleep(5)
    print('当前线程的名字是: ', threading.current_thread().name)
    time.sleep(5)

预期结果是主线称大约阻塞 10s

执行结果是:

这是主线程: MainThread
当前线程的名字是: 当前线程的名字是:  Thread-5当前线程的名字是:   Thread-1当前线程的名字是:  Thread-3
当前线程的名字是:  Thread-2

Thread-4

一共用时: 10.009864091873169
主线程结束! MainThread

和预期相符。

如果把 run 函数改成死循环,比如

def run():
    while(True):
        time.sleep(2)
        print('当前线程的名字是: ', threading.current_thread().name)
        time.sleep(2)

那么主线程就会一直阻塞,根本不会打印 “一共用时:…”,而是一直打印 “当前线程的名字是:…”

这也印证了主线程确实阻塞了,等待子线程退出,如果不退出就一直阻塞。

第二关:join(timeout)

在上面的例子中,如果子线程不返回,父线程就一直阻塞。如果需求是父线程阻塞一段时间,时间到了以后,就算子线程不返回,父线程也可以继续向下执行,那么我们可以给 join() 传入一个时间参数。意思是最多等待这么多,等不到我就继续往下执行了。

实验代码是:

import threading
import time


def run():
    time.sleep(2)
    print('当前线程的名字是: ', threading.current_thread().name)
    time.sleep(2)


if __name__ == '__main__':
    print('这是主线程:', threading.current_thread().name)
    thread_list = []

    for i in range(5):
        t = threading.Thread(target=run)
        thread_list.append(t)

    for t in thread_list:
        t.start()

    start_time = time.time()
    thread_list[0].join(2)

    print('一共用时:', time.time() - start_time)
    print('主线程结束!', threading.current_thread().name)
    

第 23 行:父线程最多等待 2s,等不到就继续执行

打印结果是:

这是主线程: MainThread
一共用时: 2.0001044273376465
主线程结束! MainThread
当前线程的名字是:  当前线程的名字是:  当前线程的名字是:  Thread-2
Thread-3
Thread-1
当前线程的名字是:  Thread-4
当前线程的名字是:  Thread-5

可以看到,父线程确实只阻塞了 2s。需要强调的是,主线程阻塞,不会对子线程造成任何影响,子线程依然执行自己的代码,根本不存在什么“主线程等待一段时间然后杀死子线程”,这是谬论。

当然,如果你传入参数是 10,主线程不一定非要等 10s,如果子线程执行了 3s 就返回了,那么主线程会立刻往下执行,而不是继续等待 7s。

例如把代码第 23 行改成

thread_list[0].join(6)

执行结果是:

这是主线程: MainThread
当前线程的名字是: 当前线程的名字是:  Thread-2 Thread-3当前线程的名字是: 当前线程的名字是: 

 当前线程的名字是:  Thread-1
 Thread-4
Thread-5
一共用时: 4.028920888900757
主线程结束! MainThread

第三关:setDaemon(True)

先上代码:

import threading
import time


def run():
    time.sleep(2)
    print('当前线程的名字是: ', threading.current_thread().name)
    time.sleep(2)


if __name__ == '__main__':
    print('这是主线程:', threading.current_thread().name)
    thread_list = []

    for i in range(5):
        t = threading.Thread(target=run)
        t.setDaemon(True)
        thread_list.append(t)

    for t in thread_list:
        t.start()

    start_time = time.time()

    print('一共用时:', time.time() - start_time)
    print('主线程结束!', threading.current_thread().name)

和之前不一样的是第 17 行,多加了 t.setDaemon(True),这句话的意思是把线程的 daemon 属性设成 True。daemon 有守护神的意思,也可以说把 t 设置为守护线程,守护谁呢?守护父线程吗?咱们后面会分析。

网上充斥着这样的解释:如果一个子线程的 daemon 被设为 True,那么父线程结束了,这个子线程就立刻被干掉。是不是这样呢?我们看执行结果:

这是主线程: MainThread
一共用时: 0.0
主线程结束! MainThread

似乎是这样,主线程一执行完,子线程就被杀死了,连打印都来不及。

我们把 15-18 行代码改一下:

    for i in range(5):
        t = threading.Thread(target=run)
        #  t.setDaemon(True)
        thread_list.append(t)
    thread_list[0].setDaemon(True)

也就是说,仅仅设置 thread_list[0] 的 daemon 为 True。预期结果是主线程一结束,thread_list[0] 就被杀死了,其余的子线程会继续执行。

结果是:

这是主线程: MainThread
一共用时: 0.0
主线程结束! MainThread
当前线程的名字是:  Thread-3当前线程的名字是:  Thread-2

当前线程的名字是:  当前线程的名字是:  Thread-5
当前线程的名字是: Thread-4
 Thread-1

这就奇怪了,不是 Thread-1 被杀死了吗,怎么后面还有打印呢?

肯定是我们的理解有问题。实际上,官方是这样解释 daemon 的:

A thread can be flagged as a “daemon thread”. The significance of this flag is that the entire Python program exits when only daemon threads are left.

以上摘自 https://docs.python.org/3/library/threading.html,黑体字是我加的。

也就是说,如果父线程执行完毕,剩下的都是守护线程,那么这些守护线程也不用再执行了,直接强制退出;如果父线程执行完毕,剩下了 3 个守护线程和 1 个非守护线程,那么这 3 个守护线程还会继续执行,当非守护线程结束的时候,这些守护线程才会强制退出。

我们做个实验:

import threading
import time


def run(cnt):
    while cnt:
        time.sleep(1)
        print('当前线程的名字是: ', threading.current_thread().name)
        time.sleep(1)
        cnt = cnt - 1
    print(threading.current_thread().name, ' 退出')

if __name__ == '__main__':
    print('这是主线程:', threading.current_thread().name)
    thread_list = []
    cnt_list = [2, 4, 4, 4, 4]

    for i in range(5):
        t = threading.Thread(target=run, args=(cnt_list[i],))
        t.setDaemon(True)
        thread_list.append(t)

    thread_list[0].setDaemon(False)

    for t in thread_list:
        t.start()

    start_time = time.time()

    print('一共用时:', time.time() - start_time)
    print('主线程结束!', threading.current_thread().name)

5-11:每个子线程执行时间不一样,根据传参 cnt 定

第 16 行,增加了cnt_list = [2, 4, 4, 4, 4],意图是让 thread_list[0] 执行时间短一些,仅执行 while 循环 2 次,其他子线程执行 4 次

18-23:先把所有子线程设为守护线程,后面再把 thread_list[0] 设为非守护线程(偷懒了)

输出结果:

这是主线程: MainThread
一共用时: 0.0
主线程结束! MainThread
当前线程的名字是: 当前线程的名字是: 当前线程的名字是:  Thread-5 当前线程的名字是:  Thread-2
Thread-1 

Thread-4
当前线程的名字是:  Thread-3
当前线程的名字是:  Thread-1当前线程的名字是:  当前线程的名字是: 当前线程的名字是:  Thread-3
当前线程的名字是:  Thread-5
 Thread-4
Thread-2

Thread-1  退出

可以发现,父线程退出后,守护线程并没有退出,当非守护线程 Thread-1 退出后,其他守护线程强制退出。

由此可见,守护线程不仅仅守护父线程,也会守护其他非守护线程。

以上就是本文的全部内容。欢迎各位老师批评斧正。

以上是关于python 多线程中的 join 和 daemon的主要内容,如果未能解决你的问题,请参考以下文章

python 多线程中的 join 和 daemon

python的多线程中的join的作用

python线程中的join(转)

Python多线程的理解和使用Threading中join()函数的理解

threading模块—Python多线程编程

python多线程之t.setDaemon(True) 和 t.join()