2. Python线程基础:threading模块入门
说实话,很多初学者一上来就问我:「Python多线程到底有没有用?」我的回答是——看场景。但不管有没有用,threading模块都是你绕不开的基础。今天我们就把它彻底搞明白。
2.1 threading模块:Python的线程工具箱
Python标准库里的threading模块,说白了就是一套帮你创建和管理线程的工具。我最早接触它是在做一个爬虫项目,那时候还傻乎乎地用_thread底层模块,结果被锁和同步搞得焦头烂额。后来换成threading,真香。
这个模块提供了什么?
- Thread类:创建线程的核心类
- Lock、RLock:互斥锁和可重入锁
- Semaphore、Event、Condition:线程同步工具
- Queue:线程安全的队列
嗯,今天我们先聚焦最基础的——Thread类。
2.2 创建线程的两种方式
我个人习惯把创建线程的方式分成两类:函数式和类式。两种都能用,但场景不同。
方式一:直接传入函数
这是最直观的方式。你定义一个普通函数,然后把它丢给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()
我在项目中遇到过一种情况:临时需要一个轻量级的后台任务,比如定时刷新缓存。这时候用函数式创建线程,代码量少,逻辑清晰。你想想看,如果每个小任务都要定义一个类,那得多累赘。
方式二:继承Thread类
当你的线程需要维护状态、或者有复杂的初始化逻辑时,继承Thread类更合适。
import threading
import time
class MyWorker(threading.Thread):
def __init__(self, name, delay):
super().__init__()
self.name = name
self.delay = delay
def run(self):
print(f"线程 {self.name} 启动")
time.sleep(self.delay)
print(f"线程 {self.name} 完成")
t = MyWorker("C", 3)
t.start()
注意,必须重写run方法。我曾经见过有人重写了start方法,结果线程根本没跑起来——因为start内部会调用run,你把它覆盖了,逻辑就断了。
核心区别:函数式适合简单任务,类式适合需要封装状态的复杂线程。
2.3 线程的启动与等待
创建完线程,下一步就是让它跑起来。这里有两个关键方法:start()和join()。
start():让线程真正跑起来
很多人以为创建Thread对象线程就开始了——不对。创建对象只是「准备好了」,调用start()才会让操作系统真正分配资源并执行。
t = threading.Thread(target=worker, args=("D",))
# 此时线程还没开始
t.start()
# 现在线程开始运行了
我踩过一个坑:直接调用run()而不是start()。结果线程代码确实执行了,但是在主线程里同步执行的,根本没有并发效果。嗯,这个错误我犯过一次就再也没忘过。
join():等待线程结束
有时候你需要等子线程干完活,再继续往下走。这时候就用join()。
import threading
import time
def slow_task():
time.sleep(3)
print("任务完成")
t = threading.Thread(target=slow_task)
t.start()
print("等待子线程...")
t.join() # 主线程阻塞在这里,直到t结束
print("子线程已结束,主线程继续")
你可以给join()传一个超时参数,比如t.join(2)表示最多等2秒。超时后主线程不再等待,但子线程仍在后台运行。这个特性我在做超时控制时经常用。
小技巧:如果你有多个线程要等,可以循环调用join()。但注意顺序——先调start()再调join(),别搞反了。
2.4 线程的daemon属性
这个属性,说实话,很多人一开始都搞不明白。我当年也是。
daemon线程,也叫守护线程。它的特点是:当所有非daemon线程都结束时,Python程序会强制退出,不管daemon线程是否还在运行。
import threading
import time
def background_task():
while True:
print("后台任务运行中...")
time.sleep(1)
t = threading.Thread(target=background_task)
t.daemon = True # 设置为守护线程
t.start()
time.sleep(3)
print("主线程结束")
# 程序退出,daemon线程被强制终止
运行这段代码,你会发现程序在3秒后退出,不会无限打印。因为主线程(非daemon)结束了,daemon线程就被「抛弃」了。
警告:daemon线程在退出时不会执行清理操作(比如关闭文件、释放锁)。所以,不要把重要的资源管理逻辑放在daemon线程里。我曾经在daemon线程里写数据库连接,结果程序退出时连接没关闭,导致连接泄漏——排查了好久才发现。
什么时候用daemon线程?
- 后台监控任务(比如心跳检测)
- 日志轮转
- 自动保存草稿
这些任务「丢了就丢了」,不影响主业务。反过来,如果你的线程需要完成收尾工作,比如写文件、发送确认消息,那就别设成daemon。
知识体系总览
下面这张图,是我梳理的本章核心脉络。你看一眼,心里就有谱了。
这张图把本章的三个核心知识点串起来了。你记住:创建 → 启动 → 控制生命周期,这就是线程编程的基本套路。
本章小结:
- 创建线程有两种方式:函数式(简单任务)和类式(复杂状态)
start()让线程真正运行,join()等待线程结束- daemon线程随主线程退出而终止,适合后台任务
- 别在daemon线程里做资源清理工作——这是血的教训
好了,线程基础就聊到这儿。下一节我们会深入锁机制,到时候你会明白为什么「线程安全」这四个字值万金。
可可精品资料库