1. 什么是线程? #
1.1 什么是线程? #
进程是程序运行时的"容器",每个进程有独立的内存空间。线程是进程内的"执行单元",一个进程可以包含多个线程,它们共享进程的内存。
主线程:Python 程序启动时默认有一个线程在跑,这就是主线程。你写的代码默认都在主线程里执行。
1.2 为什么需要多线程? #
程序默认是单线程的,代码一行行执行。如果某一步要等很久(比如等网络响应、读大文件),后面的代码就得干等着。多线程可以让程序"同时"做多件事:一个线程在等 I/O,另一个线程可以继续干活,提高效率。
通俗比喻:单线程像一个人排队办业务;多线程像开多个窗口,多人同时办。
注意:Python 有 GIL(全局解释器锁),多线程不能真正并行执行计算,但适合 I/O 密集型任务(网络、磁盘读写等),因为等待 I/O 时会释放 GIL,其他线程可以运行。
2. 什么是 threading? #
threading 是 Python 标准库中的多线程模块,提供:
- 创建线程:
Thread类 - 同步机制:锁(Lock)、事件(Event)等,防止多线程同时改同一份数据
- 线程间通信:配合
queue.Queue传递数据
下面从创建线程开始学习。
3. 创建线程 #
3.1 最常用的方式:传入函数 #
把要执行的函数传给 Thread,用 args 传参数。
# 导入 threading 和 time
import threading
import time
# 定义线程要执行的函数
def worker(name, delay):
# 打印开始信息
print(f"线程 {name} 开始")
# 模拟耗时操作(如等待网络、读文件)
time.sleep(delay)
# 打印结束信息
print(f"线程 {name} 结束")
# 创建两个线程,target 是函数名,args 是传给函数的参数(元组)
t1 = threading.Thread(target=worker, args=("A", 2))
t2 = threading.Thread(target=worker, args=("B", 1))
# 启动线程(启动后会自动调用 worker 函数)
t1.start()
t2.start()
# 主线程等待 t1 和 t2 都执行完再继续
t1.join()
t2.join()
# 所有线程结束后打印
print("所有线程已完成")说明:线程 A 睡 2 秒,线程 B 睡 1 秒,B 会先结束。join() 会阻塞主线程,直到被等待的线程结束。
3.2 继承 Thread 类(可选) #
通过继承 Thread 并重写 run() 方法,也可以自定义线程行为。一般用 3.1 的方式就够了。
# 导入 threading 和 time
import threading
import time
# 继承 Thread 类
class MyThread(threading.Thread):
# 自定义初始化,接收 name 和 delay
def __init__(self, name, delay):
# 先调用父类初始化
super().__init__()
self.name = name
self.delay = delay
# 重写 run 方法,线程启动后会执行这里的代码
def run(self):
print(f"线程 {self.name} 开始")
time.sleep(self.delay)
print(f"线程 {self.name} 结束")
# 创建并启动线程
t = MyThread("C", 1.5)
t.start()
t.join()
print("线程 C 已完成")4. 线程的常用属性和方法 #
| 方法/属性 | 作用 |
|---|---|
start() |
启动线程 |
join(timeout) |
等待线程结束,可设置超时秒数 |
name |
线程名称,可读可写 |
is_alive() |
线程是否还在运行 |
daemon |
是否为守护线程(需在 start() 前设置) |
# 导入 threading 和 time
import threading
import time
# 定义线程函数
def worker():
time.sleep(1)
# 获取当前线程对象并打印名称
print("当前线程:", threading.current_thread().name)
# 创建线程,指定名称
t = threading.Thread(target=worker, name="工作线程")
# 启动
t.start()
# 查看是否存活
print("线程是否存活:", t.is_alive())
# 等待结束
t.join()
print("线程是否存活:", t.is_alive())5. 线程同步:锁(Lock) #
多个线程同时修改同一份数据时,可能产生数据错乱。锁可以保证同一时刻只有一个线程执行某段代码。
5.1 为什么需要锁? #
下面是不加锁时,多线程同时修改计数器,结果可能不对:
# 导入 threading
import threading
# 共享变量:计数器
counter = 0
# 每个线程执行 10 万次加 1
def increment():
global counter
for _ in range(100000):
counter += 1
# 创建 10 个线程同时加
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
# 理想结果是 1000000,实际可能小于 1000000(因为数据竞争)
print("counter =", counter)5.2 使用锁保护共享数据 #
用 Lock 包住"读-改-写"这段代码,保证同一时刻只有一个线程执行。
# 导入 threading
import threading
# 共享变量
counter = 0
# 创建锁
lock = threading.Lock()
# 每个线程执行 10 万次加 1
def increment():
global counter
for _ in range(100000):
# with lock 会自动获取锁,执行完后自动释放
# 保证 counter += 1 同一时刻只有一个线程在执行
with lock:
counter += 1
# 创建 10 个线程
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
# 正确输出 1000000
print("counter =", counter)新手提示:推荐始终用 with lock:,这样即使发生异常,锁也会被正确释放。
6. 事件(Event):线程间简单信号 #
Event 用于一个线程"通知"另一个线程:某个条件已满足,可以继续了。
# 导入 threading 和 time
import threading
import time
# 创建事件对象
event = threading.Event()
# 等待事件的线程
def waiter():
print("等待事件触发...")
# 阻塞,直到 event.set() 被调用
event.wait()
print("事件已触发,继续执行")
# 触发事件的线程
def setter():
# 模拟准备时间
time.sleep(2)
print("触发事件")
# 设置事件,所有 wait() 的线程会继续
event.set()
# 启动两个线程
threading.Thread(target=waiter).start()
threading.Thread(target=setter).start()说明:event.wait() 会阻塞直到别的线程调用 event.set()。适合"准备好了再通知"这类场景。
7. 线程间通信:队列(Queue) #
多个线程之间传递数据时,用 queue.Queue 最安全。一个线程往队列里放数据,另一个线程从队列里取,无需自己加锁。
# 导入 threading、queue 和 time
import threading
import queue
import time
# 创建队列
q = queue.Queue()
# 生产者:往队列里放数据
def producer():
for i in range(5):
q.put(f"数据-{i}")
print(f"生产 数据-{i}")
time.sleep(0.5)
# 放入 None 作为结束信号
q.put(None)
# 消费者:从队列里取数据
def consumer():
while True:
item = q.get()
# 收到结束信号就退出
if item is None:
break
print(f"消费 {item}")
# 创建并启动生产者和消费者线程
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
# 等待两个线程结束
t1.join()
t2.join()
print("完成")说明:Queue 是线程安全的,多个线程同时 put 和 get 不会出错。更详细的用法见 queue 模块教程。
8. 守护线程 #
守护线程:当所有非守护线程结束时,程序会退出,不等待守护线程完成。适合做后台任务(如心跳、日志)。
# 导入 threading 和 time
import threading
import time
# 模拟后台任务
def daemon_worker():
for i in range(5):
print(f"守护线程运行中 {i}")
time.sleep(1)
print("守护线程结束")
# 创建线程,daemon=True 表示守护线程
t = threading.Thread(target=daemon_worker, daemon=True)
t.start()
# 主线程只等 2 秒
time.sleep(2)
print("主线程结束,程序退出(守护线程可能未跑完)")注意:daemon 必须在 start() 之前设置。守护线程可能被突然终止,不要用它做必须完成的任务。
9. GIL 简要说明 #
GIL(全局解释器锁)是 CPython 的一个机制,同一时刻只允许一个线程执行 Python 字节码。因此:
- CPU 密集型(大量计算):多线程无法真正并行,反而可能更慢,应改用
multiprocessing(多进程) - I/O 密集型(网络、磁盘):线程在等待 I/O 时会释放 GIL,多线程能提高效率,
threading很适合
新手只需记住:多线程适合 I/O 等待多的场景,不适合纯计算密集场景。
10. 注意事项 #
- 共享数据要加锁:多线程修改同一变量时,用
Lock保护。 - 推荐用
with lock:避免忘记释放锁。 - 线程内异常不会传到主线程:在目标函数里自己
try/except处理。 - 慎用守护线程:只在确定"可随时中断"的后台任务时使用。
11. 总结 #
| 内容 | 要点 |
|---|---|
| 创建线程 | Thread(target=函数, args=(参数,)),然后 start()、join() |
| 保护共享数据 | 用 Lock,配合 with lock: |
| 线程间信号 | 用 Event:wait() 等待,set() 触发 |
| 线程间传数据 | 用 queue.Queue |
| 守护线程 | daemon=True,程序退出时不等待 |
记忆口诀:创建用 Thread,共享用 Lock,传信用 Event,传数用 Queue。