3 - 线程 - Windows 10 - Python - 守护线程 _ 后台线程 _ 精灵线程

发布时间 2023-03-24 16:51:45作者: Loki_Severus

@


测试环境:

操作系统: Window 10
工具:Pycharm
Python: 3.7

一、守护线程 / 后台线程 / 精灵线程

首先讲解守护线程。

有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为“后台线程Daemon Thread)”,又称为“守护线程”或“精灵线程”。Python 解释器的垃圾回收线程就是典型的后台线程。

后台线程有一个特征,如果所有的前台线程都死亡了,那么后台线程会自动死亡。

用 python 的 Threading 模块调用 Thread 对象的 daemon属性可以将指定线程设置成后台线程。
下面程序将指定线程设置成后台线程,可以看到当所有的前台线程都死亡后,后台线程随之死亡。当运行的程序中只剩下后台线程时,程序就没有继续运行的必要了,所以程序也就退出了。
所有的线程 —— 主线程/子线程 默认 daemon 配置为 False

我个人有趣的见解是,既然是守护,那么如果公主凉了,骑士也得跟着凉,所以就叫守护线程,如果不是守护,那么各自过自己的生活(主线程执行完了,子线程也不用跟着退出,可以完成自己的任务),互不干涉,除非是在同一个房子里(调用同一个函数),那就得要有一把锁(线程锁),每人住一天(每个线程拿到线程锁)。

前/后台线程代码演示:

import threading

# 定义后台子线程的线程执行体与普通线程没有任何区别
def action(max):
    for i in range(max):
        print(threading.current_thread().name + "  " + str(i))
        
# 指定创建的子线程的运行函数是 action,参数是元组 () 内容是 100 , 线程名为 后台线程
t = threading.Thread(target=action, args=(100,), name='后台线程')  

# 将此线程设置成后台子线程/后台线程
# 也可在创建Thread对象时通过daemon参数将其设为后台子线程/后台线程
t.daemon = True

# 启动后台子线程/后台线程
t.start()  # 相当于是执行 action() 方法

for i in range(10):  # 运行到 10 则结束主线程
    print(threading.current_thread().name + "  " + str(i))
# -----程序执行到此处,前台线程(主线程)结束------
# 后台子线程/后台线程也应该随之结束

运行结果:
在这里插入图片描述
其实在这里应该是在主线程这里开了一个子线程,然后在设置子线程为后台线程,这种方法有别于 线程的 join() 方法,join()方法是子线程特有的堵塞主线程的方法,只有等待指定的子线程执行完毕了,主线程才能继续运行接下来的代码,注意这里是哪个线程使用了 join() 方法,那么主线程就得等这个子线程执行完毕了,才能继续运行后面的程序。

回归正题,这里的子线程 daemon 参数最为特殊的一点是,它主要的服务对象是主线程和子线程,统称为前台线程,只要所有的前台线程都结束运行了,那么后台线程也必须退出运行,我举个代码例子,应该就稍微有点理解了。

下面的代码需要一点其他知识,链接如下:
Python - 线程 - 重定向重构方法

前/后台线程代码演示:
(代码内存在两个同名同参的方法 action(max),也就是上面提到的重定向重构方法)

import threading

# 定义后台子线程的线程执行体与普通线程没有任何区别
def action(max):
    for i in range(max):
        print(threading.current_thread().name + "  " + str(i))
        
# 指定创建的子线程的运行函数是 action,参数是元组 () 内容是 100 , 线程名为 后台线程
t = threading.Thread(target=action, args=(100,), name='后台线程')  

# 将此线程设置成后台子线程/后台线程
# 也可在创建Thread对象时通过daemon参数将其设为后台子线程/后台线程
t.daemon = True

# 启动后台子线程/后台线程
t.start()  # 相当于是执行 action() 方法

# 定义后台子线程的线程执行体与普通线程没有任何区别
def action(max):
    for i in range(max):
        print(threading.current_thread().name + "  " + str(i))
        
# 指定创建的子线程的运行函数是 action,参数是元组 () 内容是 100 , 线程名为 后台线程
t1 = threading.Thread(target=action, args=(100,), name='普通线程') 
t1.start()  # 相当于是执行 action() 方法

for i in range(10):  # 运行到 10 则结束主线程
    print(threading.current_thread().name + "  " + str(i))
# -----程序执行到此处,前台线程(主线程)结束------
# 后台子线程/后台线程也应该随之结束

运行结果:
在这里插入图片描述
在这里插入图片描述
看到结果了吗?有时候感觉挺玄学的,在运行结果中,后台线程和普通线程(前台线程)有时候是可以一起运行结束并退出的,但是假如普通线程(前台线程 —— 主线程/子线程)比后台线程运行的速度还要更快,那就只能强制退出后台线程了,因此就会出现这样的错误。看着似乎挺乱的,其实只要知道了,多线程的运行是交替执行的,尤其是这里的Python线程,用的还是 Cpython 解释器,实际上是并发执行线程的

如果你看到界面的显示是一行一行的显示的,就猜到是交替执行的,只不过会显示的很快,不过也不能这么说线程就一定是交替执行的,只不过这里的python线程,应该说是 Cpython 解释器,的确是个挺奇葩的存在,因为它内部的机制 GIL,所以根本没法用线程并行,别被什么线程就一定是并行的说法给弄乱了,实际上python的多线程是并发执行的。除非换个解释器,比如Jpython之类的,不过目前主流的解释器是Cpython,当然还有一个办法是使用 CPython解释器的多进程方法来规避多线程并发,这个时候就能使用多线程并行了。

就像下面这样,后台线程和前台线程都能执行完毕,但是呢,要知道Cpython解释器 的多线程其实是并发的(由于解释器的内部机制GIL,还有一个进程只能有一个 GIL),这个线程交替执行有点玄学概念,子线程的运行,可能需要靠运气才能实现,靠抢的,靠运气的,获取Cpython解释器 的GIL,才能得到进入CPU 的机会,因此得以运行子线程。

GIL 参考链接:进程与线程 - CPython 解释器 - 多线程并行(实际并发)

后台和前台线程能够一起完成运行,如下所示:
在这里插入图片描述
还有一个更加直观的测试方法,看出前台线程没有运行了,那么后台线程也得跟着退出运行,即使后台线程没执行完毕,修改前台线程代码内循环 100 次,改为 50次,而后台线程循环 100 次不变,代码如下所示:

import threading

# 定义后台子线程的线程执行体与普通线程没有任何区别
def action(max):
    for i in range(max):
        print(threading.current_thread().name + "  " + str(i))
        
# 指定创建的子线程的运行函数是 action,参数是元组 () 内容是 100 , 线程名为 后台线程
t = threading.Thread(target=action, args=(100,), name='后台线程')  

# 将此线程设置成后台子线程/后台线程
# 也可在创建Thread对象时通过daemon参数将其设为后台子线程/后台线程
t.daemon = True

# 启动后台子线程/后台线程
t.start()  # 相当于是执行 action() 方法

# 定义后台子线程的线程执行体与普通线程没有任何区别
def action(max):
    for i in range(max):
        print(threading.current_thread().name + "  " + str(i))
        
# 指定创建的子线程的运行函数是 action,参数是元组 () 内容是 100 , 线程名为 后台线程
t1 = threading.Thread(target=action, args=(50,), name='普通线程') 
t1.start()  # 相当于是执行 action() 方法

for i in range(10):  # 运行到 10 则结束主线程
    print(threading.current_thread().name + "  " + str(i))
# -----程序执行到此处,前台线程(主线程)结束------
# 后台子线程/后台线程也应该随之结束

运行结果:
可以看到下面的 MainThread 9 其实就是主线程,运行了第10 次的结果(从 0 开始计数),因为主线程号从0开始的,可以看到主线程运行完毕了,子线程(普通线程)还在运行着,这里的子线程(普通线程)属于前台线程,运行到了第 50 次 ,而后台线程却只是运行到了第 60 次 ,因为子线程(普通线程)运行了 50 次,程序需要退出运行了,后台线程就被强制退出运行,后面的 40 次循环,就没能运行下去。
在这里插入图片描述
后台线程被强制退出运行,40次循环的计划被取消了
在这里插入图片描述
可以看到后台线程没法完成 100 次的循环,而普通线程(前台线程)率先完成了 50 次的循环,所以后台线程没法独立存在,只能是依托于前台线程(主线程与子线程/普通线程),类似于作为精灵女仆存在 —— 作为服务存在,所以不知道为啥,会叫精灵线程呢?这个有待考证,o( ̄︶ ̄)o ,有结果了,会补充说明下。

二、创建精灵线程的方法

那么该如何创建一个精灵呢?

这里提供两个方法,一种是参数法,一种是函数法。

参数法:

就是上面使用的 t.daemon = True 赋予 True ,就是指定该线程为 守护线程 / 后台线程 / 精灵线程

函数法:

利用线程带有的 setDaemon(True) 方法,将线程设置为 守护线程 / 后台线程 / 精灵线程
具体实现 t.setDaemon(True)
实际上函数法是将参数 daemon 设置为 True ,没太大神秘。

代码演示:
可以把 #t.daemon = True 注释去掉进行测试

import threading

# 定义后台子线程的线程执行体与普通线程没有任何区别
def action(max):
    for i in range(max):
        print(threading.current_thread().name + "  " + str(i))
        print(t.daemon)
        
# 指定创建的子线程的运行函数是 action,参数是元组 () 内容是 100 , 线程名为 后台线程
t = threading.Thread(target=action, args=(100,), name='后台线程')  

# 将此线程设置成后台子线程/后台线程
# 也可在创建Thread对象时通过daemon参数将其设为后台子线程/后台线程

#t.daemon = True
t.setDaemon(True)

# 启动后台子线程/后台线程
t.start()  # 相当于是执行 action() 方法


for i in range(10):  # 运行到 10 则结束主线程
    print(threading.current_thread().name + "  " + str(i))
# -----程序执行到此处,前台线程(主线程)结束------
# 后台子线程/后台线程也应该随之结束

注意,当前台线程死亡后,Python 解释器会通知后台线程死亡,但是从它接收指令到做出响应需要一定的时间。如果要将某个线程设置为后台线程,则必须在该线程启动之前进行设置。也就是说,将 daemon 属性设为 True,必须在 start() 方法调用之前进行,否则会引发 RuntimeError 异常。

参考链接

CPython - 理解伪多线程中 join() 和 sleep() 的作用

进程与线程 - CPython 解释器 - 多线程并行(实际并发)