2. threading模块入门:创建线程的两种方式、线程的启动与等待、线程的daemon属性
好,咱们正式开始玩线程了。Python 的 threading 模块,说白了就是帮你把单线程的程序变成多线程的。我刚开始接触它的时候,觉得这东西挺神奇的——明明代码是顺序写的,怎么就能同时跑起来了?
嗯,别急,咱们一步步来。这一节我会带你搞定三个核心问题:怎么创建线程、怎么控制它的生命周期、以及daemon 属性到底是个什么鬼。
2.1 创建线程的两种方式
Python 里创建线程,主流就两种路子。我个人习惯用第二种,但第一种在某些场景下也挺顺手。咱们都看看。
方式一:直接传入函数
这是最直观的方式。你把一个函数丢给 Thread 对象,它就跑起来了。我在项目中经常用它来处理一些简单的后台任务,比如日志写入、心跳检测之类的。
import threading
import time
def worker(name):
print(f"线程 {name} 开始工作")
time.sleep(2)
print(f"线程 {name} 结束工作")
# 创建线程
t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))
# 启动线程
t1.start()
t2.start()
# 等待线程结束
t1.join()
t2.join()
print("所有线程执行完毕")
你看,代码很简洁。target 指定要执行的函数,args 传参数。启动用 start(),等待用 join()。这里有个坑——千万别直接调用 worker(),那是在主线程里同步执行,不是多线程。我曾经见过新手把 start() 写成 run(),结果线程根本没跑起来,代码顺序执行了。嗯,注意一下。
方式二:继承 Thread 类
这种方式更适合需要封装复杂逻辑的场景。你想想看,如果你的线程需要维护状态、需要多个方法协作,那继承一个类就比传函数优雅多了。
import threading
import time
class MyWorker(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
self.result = None
def run(self):
print(f"线程 {self.name} 开始工作")
time.sleep(2)
self.result = f"来自 {self.name} 的结果"
print(f"线程 {self.name} 结束工作")
# 创建并启动
t1 = MyWorker("A")
t2 = MyWorker("B")
t1.start()
t2.start()
t1.join()
t2.join()
print(t1.result)
print(t2.result)
这里的关键是重写 run() 方法。启动线程时依然调用 start(),它会自动调用 run()。我建议你在 __init__ 里初始化一些状态变量,比如 self.result,这样线程结束后还能拿到返回值。
- 任务简单、逻辑单一 → 用函数方式,代码少
- 任务复杂、需要状态管理 → 用继承方式,结构清晰
2.2 线程的启动与等待
创建完线程,下一步就是让它跑起来。这里有两个核心方法:start() 和 join()。
start():让线程真正跑起来
start() 会告诉操作系统:嘿,给我开个新线程,跑这个函数。注意,一个线程对象只能调用一次 start()。如果你调两次,Python 会直接抛 RuntimeError。我踩过这个坑,当时调试了半天才发现是重复启动了。
join():等线程干完活
join() 的意思是:主线程在这里等着,直到子线程执行完毕。如果不加 join(),主线程可能直接跑完退出,子线程还没干完活呢。你想想看,如果主线程退出了,子线程可能被强制终止,数据都没写完。
join() 传一个超时参数,比如 t.join(5),表示最多等 5 秒。如果 5 秒后线程还没结束,主线程就不等了。这在处理可能卡死的线程时特别有用。
2.3 线程的 daemon 属性
这个属性,说实话,刚接触时容易搞混。我简单解释一下:daemon 线程是“守护”线程,主线程退出时,它会自动被干掉。而非 daemon 线程,主线程会等它执行完才退出。
为什么会这样?因为 Python 解释器退出时,会检查是否还有非 daemon 线程在运行。如果有,它就等着。如果全是 daemon 线程,它直接走人,不管它们死活。
import threading
import time
def daemon_worker():
while True:
print("守护线程工作中...")
time.sleep(1)
def normal_worker():
print("普通线程开始工作")
time.sleep(3)
print("普通线程结束工作")
# 创建守护线程
d = threading.Thread(target=daemon_worker, daemon=True)
# 创建普通线程
n = threading.Thread(target=normal_worker)
d.start()
n.start()
# 主线程只等普通线程
n.join()
print("主线程结束")
运行这段代码,你会发现:普通线程结束后,主线程打印完就退出了,守护线程被强制终止,连“结束工作”都没机会说。嗯,这就是 daemon 的特性。
2.4 知识体系一览
为了让你更直观地理解这一节的内容,我画了一张图。它把创建线程的两种方式、启动等待流程、以及 daemon 属性的影响都串起来了。
这张图把咱们这一节的内容都串起来了。你从上往下看:先选创建方式,然后启动等待,最后根据任务性质决定 daemon 属性。嗯,逻辑很清晰。
好了,这一节就到这里。你掌握了创建线程的两种方式、启动等待的流程、以及 daemon 属性的使用场景。下一节咱们会深入线程同步问题,到时候你会遇到锁、条件变量这些更有趣的东西。嗯,先消化一下今天的内容吧。