3 - 线程 - Windows 10 - Python - 守护线程 _ 后台线程 _ 精灵线程
@
测试环境:
操作系统: 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
异常。