线程创建与管理:使用threading模块创建线程、线程的启动与等待、守护线程与前台线程
多线程编程,说白了就是让程序同时干几件事。但很多人一上来就踩坑——线程怎么创建?启动后怎么等它结束?守护线程又是什么鬼?别急,咱们一个一个捋清楚。
核心要点:Python的threading模块是标准库,开箱即用。但用不好,轻则逻辑混乱,重则程序崩溃。我见过太多人把线程当函数调,结果程序跑飞了还不知道为什么。
1. 创建线程的两种姿势
我个人习惯用两种方式创建线程。第一种最直接,适合简单任务;第二种更灵活,适合复杂场景。
方式一:直接传入函数
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传参数。注意args必须是元组,哪怕只有一个参数也要加逗号——我刚开始写的时候就老忘这个逗号,结果报错半天找不出原因。
方式二:继承Thread类
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f"线程 {self.name} 开始工作")
time.sleep(2)
print(f"线程 {self.name} 结束")
t1 = MyThread("C")
t2 = MyThread("D")
t1.start()
t2.start()
t1.join()
t2.join()
这种方式的好处是,你可以把线程相关的数据和逻辑封装在一起。我在项目中处理网络请求时,就喜欢用这种方式——每个线程有自己的连接池、重试逻辑,代码清晰多了。
我的建议:简单任务用函数式,复杂任务用继承式。别为了炫技硬上继承,代码可读性才是第一位的。
2. 启动与等待:start()和join()的玄机
很多新手会问:为什么不用run()直接调用?嗯,这里要注意——run()只是普通方法调用,不会启动新线程。start()才是真正的线程启动入口。
为什么会这样?因为start()会调用操作系统的底层接口创建新线程,然后在新线程里执行run()方法。你直接调run(),那跟普通函数调用没区别。
警告:千万不要直接调用run()!我曾经在代码审查时看到有人这么写,结果程序跑起来跟单线程一样慢,排查了半天才发现是这个问题。
再说join()。它的作用是等待线程结束。如果不调join(),主线程可能先跑完,子线程还在后台跑。这在某些场景下没问题,但如果你需要子线程的结果,就必须等。
# 错误的做法
t = threading.Thread(target=worker, args=("E",))
t.start()
print("主线程结束") # 可能先于子线程打印
# 正确的做法
t = threading.Thread(target=worker, args=("F",))
t.start()
t.join() # 等子线程结束
print("主线程结束") # 一定在子线程之后打印
3. 守护线程 vs 前台线程
这个概念很多人搞混。说白了,守护线程就是后台跑的小弟,前台线程是主角。
前台线程:主程序必须等所有前台线程结束才能退出。
守护线程:主程序退出时,守护线程会被强制终止。
设置守护线程很简单,在start()之前设置daemon=True:
def background_task():
while True:
print("守护线程在后台运行...")
time.sleep(1)
t = threading.Thread(target=background_task, daemon=True)
t.start()
time.sleep(3)
print("主线程结束,守护线程会被强制终止")
运行这段代码,你会发现主线程结束后,守护线程也停了。如果去掉daemon=True,程序会一直跑下去,因为前台线程的while循环永远不会结束。
避坑指南:我曾经写过一个日志收集服务,用守护线程定期刷日志。结果有一次主程序异常退出,守护线程里的日志还没写完就被杀了,导致丢了一部分日志。后来我改成前台线程,加上优雅关闭的逻辑,才彻底解决。
4. 知识体系图
下面这张图帮你理清线程创建与管理的核心逻辑:
5. 实战中的几个坑
最后分享几个我踩过的坑,你遇到了能少走弯路:
| 常见问题 | 原因 | 解决方案 |
|---|---|---|
| 线程启动后不执行 | 忘了调start(),或者调了run() | 确认调用的是start(),不是run() |
| 主线程提前退出 | 没调join()等待子线程 | 在需要等待的地方加join() |
| 守护线程数据丢失 | 主线程退出时强制终止 | 改用前台线程+优雅关闭 |
| 线程间数据混乱 | 共享变量没加锁 | 使用Lock或Queue(后续章节会讲) |
小技巧:调试多线程程序时,可以给每个线程起个有意义的名字。threading.current_thread().name 能帮你快速定位问题线程。我习惯用"worker-1"、"worker-2"这种命名方式,一看就知道是哪个。
嗯,线程创建与管理就这些内容。记住:start()启动,join()等待,daemon控制生命周期。这三个点搞明白了,多线程编程就迈出了第一步。