threading 模块是 Python 的标准库,所以你不需要“安装”它,只要你安装了 Python,就已经拥有了 threading 模块。
你只需要在代码中 import 它即可使用。
import threading
什么是 threading?为什么需要它?
threading 模块允许你在一个程序中创建和管理多个线程。线程是程序执行流的最小单位。
想象一下你正在厨房做饭:
- 单线程程序:你只有一个你,你必须先淘米(任务A),然后等米饭煮上,再切菜(任务B),切菜的时候,你就不能去管米饭了,这就是“顺序执行”,效率不高。
- 多线程程序:你变成了你和你自己(两个线程),一个你负责淘米并看着它煮(任务A),另一个你利用煮饭的空闲时间去切菜(任务B),虽然你还是在同一个厨房(同一个进程)里,但两个任务可以“进行,大大提高了效率。
使用 threading 的主要目的就是实现“并发”(Concurrency),让程序看起来像是“执行多个任务,从而提高程序的执行效率,特别是在处理 I/O 密集型任务时(如网络请求、文件读写)。
threading 的基本使用
threading 模块的核心是 Thread 类,我们将通过几个例子来学习如何使用它。
创建和启动线程(最简单的方式)
有两种主要的方式来创建线程:
传递一个函数给 Thread
import threading
import time
# 定义一个函数,这个函数将被线程执行
def print_numbers():
for i in range(1, 6):
print(f"线程 {threading.current_thread().name} 正在打印: {i}")
time.sleep(1) # 模拟耗时操作
# 定义另一个函数
def print_letters():
for letter in 'ABCDE':
print(f"线程 {threading.current_thread().name} 正在打印: {letter}")
time.sleep(1.5) # 模拟耗时操作
# --- 主程序 ---
# 创建线程对象
# target: 指定线程要执行的函数
# name: 给线程起个名字
thread1 = threading.Thread(target=print_numbers, name="数字线程")
thread2 = threading.Thread(target=print_letters, name="字母线程")
# 启动线程(这会让操作系统开始调度该线程)
# 注意:调用 start() 后,线程会立即开始执行,但不会马上执行完
thread1.start()
thread2.start()
# 等待线程执行完毕
# 如果没有 join(),主程序会继续往下执行,不会等待子线程
# 这会导致子线程还没跑完,主程序就退出了
thread1.join()
thread2.join()
print("所有线程执行完毕!")
输出分析: 你会发现数字和字母是交替打印的,而不是等一个完全打印完再打印另一个,这就是多线程并发执行的效果。
继承 threading.Thread 类
这种方式更面向对象,适合复杂的线程逻辑。
import threading
import time
class MyThread(threading.Thread):
def __init__(self, name):
# 必须调用父类的初始化方法
super().__init__(name=name)
# 必须重写 run() 方法,这是线程的入口点
def run(self):
for i in range(1, 4):
print(f"自定义线程 {self.name} 正在运行: {i}")
time.sleep(1)
# --- 主程序 ---
t = MyThread("自定义线程实例")
t.start() # 启动线程,它会自动调用 run() 方法
t.join() # 等待线程结束
print("自定义线程执行完毕!")
线程同步:避免“竞态条件”
当多个线程同时读写同一个共享资源(比如一个全局变量)时,就可能会发生“竞态条件”(Race Condition),导致数据不一致。
为了解决这个问题,我们需要使用“锁”(Lock)。
import threading
# 共享资源
shared_counter = 0
# 创建一个锁对象
lock = threading.Lock()
def increment_counter():
global shared_counter
for _ in range(100000):
# 获取锁
# 如果锁已经被其他线程获取,那么这个线程会在这里阻塞,直到锁被释放
lock.acquire()
try:
# 在这 critical section (临界区) 内,代码是线程安全的
shared_counter += 1
finally:
# 一定要释放锁!否则其他线程将永远等待
# 使用 try...finally 可以确保锁一定会被释放,即使代码块内发生异常
lock.release()
def decrement_counter():
global shared_counter
for _ in range(100000):
with lock: # 'with' 语句是更推荐的方式,它会自动处理 acquire() 和 release()
shared_counter -= 1
# --- 主程序 ---
t1 = threading.Thread(target=increment_counter)
t2 = threading.Thread(target=decrement_counter)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"counter 的值是: {shared_counter}")
# 理想情况下,最终结果应该是 0,如果没有锁,结果可能是任何随机数。
with lock vs lock.acquire()/release():
with lock:是 Python 的上下文管理器语法,更简洁、更安全,它会自动在代码块执行完毕后释放锁,即使代码块中抛出了异常。lock.acquire()和lock.release()需要手动配对,容易忘记release()导致死锁。
线程间通信:使用 Queue
线程间直接共享变量和锁会比较复杂且容易出错,一个更安全、更高级的通信方式是使用 queue.Queue。
Queue 是一个线程安全的队列,一个线程可以把数据“放入”队列,另一个线程可以从队列中“取出”数据。
import threading
import queue
import time
# 创建一个线程安全的队列
# maxsize=0 表示队列大小没有限制
q = queue.Queue()
def producer():
"""生产者线程:向队列中放入数据"""
for i in range(1, 6):
print(f"生产者: 准备放入数据 {i}")
q.put(i) # put() 是阻塞的,如果队列满了,它会等待
time.sleep(1)
def consumer():
"""消费者线程:从队列中取出数据"""
while True:
try:
# get() 是阻塞的,如果队列是空的,它会等待
# 可以设置超时时间,q.get(timeout=5)
item = q.get()
print(f"消费者: 取出数据 {item}")
q.task_done() # 告诉队列这个任务已经处理完毕
if item == 5: # 当取出特定数据后退出循环
break
except queue.Empty:
print("队列已空,消费者退出。")
break
# --- 主程序 ---
p = threading.Thread(target=producer, name="生产者")
c = threading.Thread(target=consumer, name="消费者")
p.start()
c.start()
# 等待生产者线程结束
p.join()
# 等待队列中所有任务都被处理完毕
# 如果消费者处理速度慢,主线程会在这里等待
q.join()
print("所有任务处理完毕!")
重要概念和注意事项
-
GIL (Global Interpreter Lock - 全局解释器锁)
- 这是 CPython (Python 的标准实现) 的一个特性。
- 核心点:在任何时刻,只有一个线程在解释器中执行 Python 字节码。
- 对多线程的影响:这意味着 Python 的多线程并不能实现真正的“并行”(Parallelism)来利用多核 CPU 的优势,它更适合处理 I/O 密集型任务,因为当一个线程在等待 I/O(如网络请求)时,GIL 会被释放,其他线程就可以获得 GIL 并执行。
- CPU 密集型任务:对于需要大量计算的任务(如数值计算),多线程由于 GIL 的存在,可能比单线程还慢,对于这类任务,应该使用
multiprocessing模块,它会创建多个进程,每个进程有自己的 Python 解释器和内存空间,可以真正并行运行。
-
死锁
- 当两个或多个线程互相等待对方释放锁时,就会发生死锁,导致所有线程都无法继续执行。
- 避免方法:
- 尽量减少锁的持有时间。
- 按照固定的顺序获取多个锁。
- 使用
with语句,确保锁一定会被释放。
-
start()vsrun()thread.start():启动线程,让操作系统去调度执行,它会调用线程的run()方法。thread.run():直接调用run()方法中的代码,这不会创建新的线程,而是在当前线程中同步执行代码。
| 功能 | 如何实现 | 何时使用 |
|---|---|---|
| 导入模块 | import threading |
永远是第一步 |
| 创建线程 | threading.Thread(target=my_func) 或 class MyThread(threading.Thread) |
当需要并发执行任务时 |
| 启动线程 | thread.start() |
创建线程后,必须调用此方法 |
| 等待线程 | thread.join() |
当主线程需要等待子线程完成时 |
| 同步访问共享资源 | with lock: |
当多个线程读写同一个变量时,防止数据错乱 |
| 线程间安全通信 | import queueq = queue.Queue()q.put() / q.get() |
当一个线程需要向另一个线程传递数据时,这是推荐的方式 |
| CPU 密集型任务 | import multiprocessing |
当任务计算量很大,需要利用多核 CPU 时 |
希望这份详细的指南能帮助你理解和使用 Python 的 threading 模块!
