Python并发编程基础——03、daemon线程和join()方法
摘要本文围绕 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, groupNone, targetNone, nameNone, args(), kwargsNone, *, daemonNone): ... ... 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(targetworker, args(t1, 1), daemonNone) 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线程全部终止Falsenon_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(fthreading {name} enter!) time.sleep(interval) end round(time.time(), 4) print(fthreading {name} finished起始于{start}结束于{end}历时{end - start}{threading.enumerate()}) t1 threading.Thread(namethreading t1, targetworker, args(t1, 3), daemonNone) t1.setDaemon(True) t1.start() t2 threading.Thread(namethreading t2, targetworker, args(t2, 6), daemonNone) 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(fthreading {name} enter!) time.sleep(interval) end round(time.time(), 4) print(fthreading {name} finished起始于{start}结束于{end}历时{end - start}{threading.enumerate()}) t1 threading.Thread(namethreading t1, targetworker, args(t1, 6), daemonNone) t1.setDaemon(True) t1.start() t2 threading.Thread(namethreading t2, targetworker, args(t2, 3), daemonNone) 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(fthreading {name} enter!) time.sleep(interval) end round(time.time(), 4) print(fthreading {name} finished起始于{start}结束于{end}历时{end - start}{threading.enumerate()}) t1 threading.Thread(namethreading t1, targetworker, args(t1, 6), daemonNone) t1.setDaemon(True) t1.start() t2 threading.Thread(namethreading t2, targetworker, args(t2, 3), daemonNone) 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线程状态mainFalse无事可做等待non-deamon线程结束t1True3秒后无事可做t2False6秒后无事可做进程结束线程daemon线程状态mainFalse无事可做等待non-deamon线程结束t1True6秒后无事可做3秒后进程结束t1被终止t2False3秒后无事可做进程结束线程daemon线程状态mainFalse8秒后无事可做进程结束t1True6秒后无事可做t2False3秒后无事可做四、线程对象的join()方法方法原型 def join(self, timeoutNone): 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, interval1): print(f{name} enter!) time.sleep(interval) print(f{name} finished!) t1 threading.Thread(namethreading t1, targetworker, args(t1, 6), daemonNone) 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(targetworker2, daemonNone) daemon进程 t2.start() t2.join() t1线程等待t2线程结束 t1 threading.Thread(targetworker1, daemonTrue) daemon进程 t1.start() t1.join() 主线程等待t1线程结束 time.sleep(4) print(Main Thread Exits) print(threading.activeCount()) print(threading.enumerate())