☰
Python并发编程基础——03、daemon线程和join()方法
2026/10/16 12:24:38 网站建设 项目流程

摘要:本文围绕 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线程全部终止
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(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线程状态
mainFalse无事可做,等待non-deamon线程结束
t1True3秒后无事可做
t2False6秒后无事可做,进程结束
线程daemon线程状态
mainFalse无事可做,等待non-deamon线程结束
t1True6秒后无事可做,3秒后进程结束,t1被终止
t2False3秒后无事可做,进程结束
线程daemon线程状态
mainFalse8秒后无事可做,进程结束
t1True6秒后无事可做
t2False3秒后无事可做

四、线程对象的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())

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询