摘要:本文围绕 Python 线程对象的 daemon 属性展开,系统讲解 daemon 的概念、线程对象的相关方法及其对线程生命周期的影响。通过多组可运行代码实验,对比 daemon 线程与 non-daemon 线程在进程结束时的不同表现,并进一步介绍 join() 方法在阻塞等待线程结束、控制线程间从属关系方面的作用,帮助读者理解 Python 多线程的调度与退出机制。
目录
一、什么是daemon?
二、线程对象的方法
三、daemon属性的作用
3.1、python的线程模型
3.2、通过代码说明
四、线程对象的join()方法
五、线程间互相等待
一、什么是daemon?
我们先不刨析原理,daemon是线程对象的一个属性,线程类的原型如下:
class Thread: """A class that represents a thread of control. This class can be safely subclassed in a limited fashion. There are two ways to specify the activity: by passing a callable object to the constructor, or by overriding the run() method in a subclass. """ _initialized = False def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None): ... ... if daemon is not None: self._daemonic = daemon else: self._daemonic = current_thread().daemon ... ...可见线程对象创建时,如果未指定其daemon属性,则跟随创建它的当前线程的daemon,主线程的daemon默认为False,可以联想到,如果子线程t1和子线程的子线程t2都未指定daemon,那么t2的daemon源于t1,而t1的daemon源于主线程。
二、线程对象的方法
| 方法名称 | 含义 |
| isDaemon() | 返回当前线程是否是daemon线程的判断结果 |
| setDaemon(bool) | 修改线程对象的daemon属性 |
import threading m = threading.current_thread() print(f"主线程的daemon属性默认为:{m.isDaemon()}") t1 = threading.Thread(target=worker, args=('t1', 1), daemon=None) print(f"子线程t1的daemon属性为:{t1.isDaemon()}") t1.setDaemon(True) print(f"修改子线程t1的daemon属性为:{t1.isDaemon()}") print("===end===")主线程的daemon属性默认为:False 子线程t1的daemon属性为:False 修改子线程t1的daemon属性为:True ===end===三、daemon属性的作用
daemon属性与线程的生命周期息息相关,其直接决定了python进程结束后,其余运行中的子线程该何去何从,凡是daemon属性为True的线程对象都被叫做守护或后台线程。
| 当daemon为 | 如果 | 那么 |
| True | 主线程(python进程)结束 | 运行中的daemon线程全部终止 |
| False | non_daemon线程未结束 | 主线程(python进程)等待所有non_daemon线程结束后再结束 |
3.1、python的线程模型
对于多核CPU,启动python程序后python.exe将会作为一个进程运行在众多CPU核的一个核上,假设本程序中仅有两个子线程,每个线程任务仅做time.sleep(timeout)这种I/O密集型操作(等待键盘输入、读取写入文件等都属于I/O密集型操作)。I/O密集型操作不会占用CPU资源,此时线程由于等待I/O操作完成而进入睡眠(等待)状态,CPU也不会闲着,而是由操作系统启动另一个就绪状态的线程,当上一个线程的I/O操作结束后,线程会进入就绪状态等待CPU调度,当CPU调度该线程后继续执行后续任务,以此类推继续执行直至完成整个进程。
扩充知识:如果线程中有CPU密集型操作(各种算法运算逻辑),在执行CPU密集型任务时,操作系统无法为CPU分配其他任务,只能等到时间片内线程任务执行结束或操作系统时间片结束,即使启动了多线程,但是四舍五入这些线程也是是并发串行执行的。这就是为什么很多人会说python的多线程是假的,现在看来这个观点太过绝对,因为I/O密集型操作还是可以并行执行的,而python的多进程模型multiprocessing是可以达成其他语言所谓的多线程效果的。
理清楚python线程模型的线程调度逻辑后,我们来看下图:
3.2、通过代码说明
daemon线程早于non-deamon线程结束
def worker(name, interval): start = round(time.time(), 4) print(f"threading {name} enter!") time.sleep(interval) end = round(time.time(), 4) print(f"threading {name} finished,起始于{start},结束于{end},历时{end - start},{threading.enumerate()}") t1 = threading.Thread(name="threading t1", target=worker, args=('t1', 3), daemon=None) t1.setDaemon(True) t1.start() t2 = threading.Thread(name="threading t2", target=worker, args=('t2', 6), daemon=None) t2.start()threading t1 enter! threading t2 enter! threading t1 finished,起始于1792030805.1987,结束于1792030808.2119,历时3.013200044631958,[<_MainThread(MainThread, stopped 7072)>, <Thread(threading t1, started daemon 19252)>, <Thread(threading t2, started 10044)>] threading t2 finished,起始于1792030805.1996,结束于1792030811.201,历时6.001399993896484,[<_MainThread(MainThread, stopped 7072)>, <Thread(threading t2, started 10044)>]deamon线程t1的任务启动后,I/O阻塞3秒
non-deamon线程t2的任务启动后,I/O阻塞6秒
线程t1、t2几乎同时启动,并全部运行结束后抓线程才结束
non-deamon线程早于deamon线程结束
def worker(name, interval): start = round(time.time(), 4) print(f"threading {name} enter!") time.sleep(interval) end = round(time.time(), 4) print(f"threading {name} finished,起始于{start},结束于{end},历时{end - start},{threading.enumerate()}") t1 = threading.Thread(name="threading t1", target=worker, args=('t1', 6), daemon=None) t1.setDaemon(True) t1.start() t2 = threading.Thread(name="threading t2", target=worker, args=('t2', 3), daemon=None) t2.start()threading t1 enter! threading t2 enter! threading t2 finished,起始于1792031285.4778,结束于1792031288.4888,历时3.011000156402588,[<_MainThread(MainThread, stopped 8900)>, <Thread(threading t1, started daemon 8684)>, <Thread(threading t2, started 21788)>]deamon线程t1的任务启动后,I/O阻塞6秒
non-deamon线程t2的任务启动后,I/O阻塞3秒
线程t1、t2几乎同时启动,但是只看到了t2的正常结束时打印的信息,由于non-daemon线程提前结束,此时主线程发现进程中没有未结束的non-deamon线程,便直接结束,此时还未结束的deamon线程也会被强制终止。
主线程主动等待线程结束
def worker(name, interval): start = round(time.time(), 4) print(f"threading {name} enter!") time.sleep(interval) end = round(time.time(), 4) print(f"threading {name} finished,起始于{start},结束于{end},历时{end - start},{threading.enumerate()}") t1 = threading.Thread(name="threading t1", target=worker, args=('t1', 6), daemon=None) t1.setDaemon(True) t1.start() t2 = threading.Thread(name="threading t2", target=worker, args=('t2', 3), daemon=None) t2.start() time.sleep(8) print("===end===")我们在主线程中加入了8秒的I/O阻塞延时操作,此时不论是non-deamon和deamon线程都可以在这个时间内结束,我们可以看到如下运行结果:
threading t1 enter! threading t2 enter! threading t2 finished,起始于1792031520.0214,结束于1792031523.0311,历时3.009700059890747,[<_MainThread(MainThread, started 2676)>, <Thread(threading t1, started daemon 11004)>, <Thread(threading t2, started 25264)>] threading t1 finished,起始于1792031520.0214,结束于1792031526.0364,历时6.015000104904175,[<_MainThread(MainThread, started 2676)>, <Thread(threading t1, started daemon 11004)>] ===end===总结
| 线程 | daemon | 线程状态 |
| main | False | 无事可做,等待non-deamon线程结束 |
| t1 | True | 3秒后无事可做 |
| t2 | False | 6秒后无事可做,进程结束 |
| 线程 | daemon | 线程状态 |
| main | False | 无事可做,等待non-deamon线程结束 |
| t1 | True | 6秒后无事可做,3秒后进程结束,t1被终止 |
| t2 | False | 3秒后无事可做,进程结束 |
| 线程 | daemon | 线程状态 |
| main | False | 8秒后无事可做,进程结束 |
| t1 | True | 6秒后无事可做 |
| t2 | False | 3秒后无事可做 |
四、线程对象的join()方法
"""方法原型""" def join(self, timeout=None): """Wait until the thread terminates. This blocks the calling thread until the thread whose join() method is called terminates -- either normally or through an unhandled exception or until the optional timeout occurs. When the timeout argument is present and not None, it should be a floating point number specifying a timeout for the operation in seconds (or fractions thereof). As join() always returns None, you must call is_alive() after join() to decide whether a timeout happened -- if the thread is still alive, the join() call timed out. When the timeout argument is not present or None, the operation will block until the thread terminates. A thread can be join()ed many times. join() raises a RuntimeError if an attempt is made to join the current thread as that would cause a deadlock. It is also an error to join() a thread before it has been started and attempts to do so raises the same exception. """ ... ...join()用于阻塞线程的启动者,让启动者等待它若干秒或一直等待它执行结束
被等待的子线程.join(time_out),被阻塞者为子线程的启动者
def worker(name, interval=1): print(f"{name} enter!") time.sleep(interval) print(f"{name} finished!") t1 = threading.Thread(name="threading t1", target=worker, args=('t1', 6), daemon=None) t1.start() """t1线程对象在主线程中启动,主线程即其启动者""" t1.join(2) """t1在此处阻塞调用者2秒""" print("-" * 30) t1.join(2) """t1在此处阻塞调用者2秒""" print("+" * 30) t1.join() """t1在此处永久阻塞,直至t1运行结束""" print("=" * 30)t1 enter! ------------------------------ ++++++++++++++++++++++++++++++ t1 finished! ==============================注意事项
线程对象的join()方法使用前,线程对象必须被启动过,否则报错:
即使join()方法调用时刻,线程已经结束了,也是允许的,只不过遵从timeout阻塞父线程若干秒,或判断已经结束后,不阻塞继续执行。
五、线程间互相等待
考虑deamon对线程生命周期的影响时,子线程只将主线程作为参照,即便是子线程的子线程,其生命周期也只与主线程相关。只有join()方法才会涉及线程之间的从属关系,子线程只阻塞属主线程。
def worker2(): while True: """无限循环任务""" pass def worker1(): t2 = threading.Thread(target=worker2, daemon=None) """daemon进程""" t2.start() t2.join() """t1线程等待t2线程结束""" t1 = threading.Thread(target=worker1, daemon=True) """daemon进程""" t1.start() t1.join() """主线程等待t1线程结束""" time.sleep(4) print("====Main Thread Exits====") print(threading.activeCount()) print(threading.enumerate())