“多线程”是 Python 中实现并发的一种重要方式,就是让一个程序能够同时“做多件事”,就像一个人可以一边听音乐一边工作一样。

目录
- 为什么需要多线程?—— 解决痛点
- 核心概念:进程 vs. 线程
- Python 中的多线程:
threading模块- 创建和启动线程
join()方法:等待线程结束- 线程安全与锁 (
Lock) - 线程间通信:
Queue队列
- Python 多线程的“阿喀琉斯之踵”:GIL (全局解释器锁)
- 何时使用多线程?何时避免?
- 实战案例:多线程下载器
为什么需要多线程?—— 解决痛点
想象一个场景:你正在写一个 Python 脚本,需要从一个网站下载 10 个文件,如果你用一个单线程的方式,代码会是这样:
# 伪代码
for url in url_list:
download(url) # 下载一个文件,需要等待它完成
print(f"下载完成: {url}")
这个脚本的执行过程是:下载完第一个文件,才开始下载第二个,以此类推,在下载文件时,程序大部分时间都在等待网络响应,CPU 是空闲的,这显然效率很低。
多线程就是为了解决这个问题:我们可以创建 10 个“线程”,每个线程负责下载一个文件,这样,当一个线程在等待网络时,CPU 可以切换到另一个正在准备下载的线程,从而实现“下载,大大提高了程序的效率。
多线程主要适用于处理 I/O 密集型任务:

- I/O 密集型:程序大部分时间都在等待外部操作完成,如网络请求、文件读写、数据库查询等。
- CPU 密集型:程序大部分时间都在进行大量的计算,如科学计算、图像处理、数据分析等。
核心概念:进程 vs. 线程
在深入 Python 多线程之前,必须理解这两个概念的区别:
| 特性 | 进程 | 线程 |
|---|---|---|
| 定义 | 操作系统进行资源分配和调度的基本单位。 | 进程中的一个执行单元,是 CPU 调度的基本单位。 |
| 资源 | 拥有独立的内存空间和系统资源,一个进程崩溃不会影响其他进程。 | 共享进程内的内存和资源,一个线程崩溃可能导致整个进程崩溃。 |
| 开销 | 创建和销毁进程的开销大,因为需要分配独立的内存空间。 | 创建和销毁线程的开销小。 |
| 通信 | 进程间通信复杂,需要通过 IPC (Inter-Process Communication) 机制,如管道、队列。 | 线程间通信简单,可以直接读写共享变量(但需要注意线程安全)。 |
| 关系 | 进程是线程的容器,一个进程可以包含一个或多个线程。 | 线程是进程的一部分。 |
一个生动的比喻:
- 进程 就像一个 工厂,它拥有自己的土地、厂房、设备(内存、文件等)。
- 线程 就像工厂里的 工人,多个工人在同一个工厂里工作,共享工厂的资源(工具、材料)。
- 多进程就是开多个工厂。
- 多线程就是在同一个工厂里雇佣多个工人。
Python 中的多线程:threading 模块
Python 提供了内置的 threading 模块来支持多线程编程。
1 创建和启动线程
有两种主要方式来创建线程:

继承 threading.Thread 类
import threading
import time
def download_file(url):
"""模拟下载文件"""
print(f"开始下载: {url}")
time.sleep(2) # 模拟耗时操作,如网络请求
print(f"下载完成: {url}")
# 创建线程对象
# target 参数指定线程要执行的函数
# args 参数是一个元组,用于向函数传递参数
thread1 = threading.Thread(target=download_file, args=("http://file1.com",))
thread2 = threading.Thread(target=download_file, args=("http://file2.com",))
# 启动线程 (此时线程开始执行,但主线程不会等待它)
thread1.start()
thread2.start()
# 主线程继续执行
print("主线程继续执行...")
使用 threading.Thread 构造函数直接传递函数
这种方式更简洁,适合不需要继承 Thread 类的简单场景。
import threading
import time
def play_music():
"""模拟播放音乐"""
print("开始播放音乐")
time.sleep(5)
print("音乐播放结束")
# 创建并启动线程
music_thread = threading.Thread(target=play_music)
music_thread.start()
print("主线程在听音乐的同时可以做别的事...")
2 join() 方法:等待线程结束
当你启动一个线程后,主线程会立即继续往下执行,而不会等待子线程执行完毕,如果主线程在子线程结束前就退出了,那么整个程序(包括所有子线程)都会被强制终止。
thread.join() 的作用就是 阻塞主线程,直到调用 join() 的那个子线程执行完毕。
import threading
import time
def task():
print("子线程开始...")
time.sleep(2)
print("子线程结束...")
t = threading.Thread(target=task)
t.start()
# 主线程在这里暂停,等待子线程 t 执行完毕
print("主线程等待子线程...")
t.join()
# 只有当子线程 t 结束后,这行代码才会被执行
print("所有线程都执行完毕,程序退出。")
3 线程安全与锁 (Lock)
当多个线程同时读写同一个共享资源时,就会发生 竞态条件,导致数据混乱。
示例:银行账户取钱(不安全)
import threading
balance = 100 # 共享资源
def withdraw(amount):
global balance
# 检查余额
if balance >= amount:
# 模拟网络延迟,在这段时间内,CPU 可能会切换到另一个线程
# 这就是导致问题的根源
threading.Event().wait(0.1)
# 执行取款
balance -= amount
print(f"取款 {amount} 成功,剩余余额: {balance}")
else:
print(f"余额不足,无法取款 {amount}")
# 创建并启动两个线程,同时取钱
t1 = threading.Thread(target=withdraw, args=(80,))
t2 = threading.Thread(target=withdraw, args=(50,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"最终账户余额: {balance}") # 预期结果是 20,但实际可能是 20 或 -30
如何解决?使用锁 (Lock)
锁就像一个“卫生间”,一次只能有一个人进去,线程在访问共享资源前,先获取锁,访问完后,再释放锁。
import threading
balance = 100
lock = threading.Lock() # 创建一个锁
def withdraw_with_lock(amount):
global balance
# 获取锁
lock.acquire()
try:
if balance >= amount:
threading.Event().wait(0.1)
balance -= amount
print(f"取款 {amount} 成功,剩余余额: {balance}")
else:
print(f"余额不足,无法取款 {amount}")
finally:
# 释放锁 (非常重要!通常用 try-finally 确保锁一定会被释放)
lock.release()
t1 = threading.Thread(target=withdraw_with_lock, args=(80,))
t2 = threading.Thread(target=withdraw_with_lock, args=(50,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"最终账户余额: {balance}") # 输出结果永远是 20
with lock 语句(推荐用法)
with 语句可以自动处理锁的获取和释放,即使发生异常也能保证锁被释放,代码更简洁安全。
def withdraw_with_with(amount):
global balance
with lock: # 自动 acquire() 和 release()
if balance >= amount:
threading.Event().wait(0.1)
balance -= amount
print(f"取款 {amount} 成功,剩余余额: {balance}")
else:
print(f"余额不足,无法取款 {amount}")
4 线程间通信:Queue 队列
当多个线程需要协作时,一个线程产生的数据需要传递给另一个线程,直接使用共享变量容易出错,而 queue.Queue 是一个线程安全的队列,专门用于线程间通信。
Queue 提供了 .put() 方法来添加数据,和 .get() 方法来获取数据。
import threading
import queue
import time
# 创建一个线程安全的队列
q = queue.Queue()
def producer():
"""生产者:向队列中添加数据"""
for i in range(5):
print(f"生产者: 放入数据 {i}")
q.put(i)
time.sleep(0.5)
def consumer():
"""消费者:从队列中取出数据"""
while True:
try:
# 从队列中获取数据,如果队列为空,会阻塞
item = q.get(timeout=2) # 设置超时,避免无限等待
print(f"消费者: 取到数据 {item},正在处理...")
time.sleep(1)
except queue.Empty:
print("消费者: 队列为空,退出。")
break
# 创建并启动线程
p = threading.Thread(target=producer)
c = threading.Thread(target=consumer)
p.start()
# 启动消费者前,可以稍等一下,确保生产者已经开始
time.sleep(0.2)
c.start()
# 等待生产者完成
p.join()
# 通知消费者队列已结束,可以退出了
# 一种简单的方式是向队列中放入一个特殊的“哨兵”值
# 但在这个例子中,我们使用了超时机制来优雅地退出
# 在实际应用中,生产者可以在任务完成后,调用 q.task_done() 和 q.join()
# 但这里为了简单,我们让消费者超时后退出。
c.join()
print("生产者和消费者线程都结束了。")
Python 多线程的“阿喀琉斯之踵”:GIL (全局解释器锁)
这是理解 Python 多线程最关键的一点!
GIL 是什么? GIL 是 Python 解释器(特别是 CPython)中的一个互斥锁,它确保在任何时刻,只有一个线程能执行 Python 字节码。
GIL 带来的影响:
- 对于 CPU 密集型 任务,多线程并不能真正实现并行,因为无论你有多少个线程,同一时间只有一个线程在运行,它只是通过快速切换线程来“看起来像”在同时运行,这被称为“并发”而非“并行”,在这种情况下,多线程的性能可能还不如单线程,因为线程切换本身也有开销。
- 对于 I/O 密集型 任务,多线程仍然非常有效,因为当一个线程在等待 I/O(如网络请求)时,GIL 会被释放,其他线程就可以获得 GIL 并开始执行,这极大地提高了程序的效率。
如何绕过 GIL? 如果你需要进行 CPU 密集型的并行计算,应该使用:
- 多进程 (
multiprocessing模块):每个进程有自己的 Python 解释器和内存空间,不受 GIL 限制,但进程创建和通信的开销比线程大。 - C 扩展:将计算密集型的部分用 C 语言编写,并通过 Python 调用,C 代码在执行时可以释放 GIL。
- 其他实现:如 Jython (运行在 JVM 上) 或 IronPython (运行在 .NET 上) 没有 GIL。
何时使用多线程?何时避免?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| I/O 密集型 | 多线程 (threading) |
线程在等待 I/O 时会释放 GIL,其他线程可以继续工作,效率高。 |
| CPU 密集型 | 多进程 (multiprocessing) |
每个 进程独立运行,不受 GIL 限制,能真正利用多核 CPU。 |
| 需要大量并发连接 | 多线程 或 异步编程 (asyncio) |
如 Web 服务器,用多线程处理每个连接非常合适。 |
| 简单任务,提高响应速度 | 多线程 | 如 GUI 程序,用一个线程处理耗时操作,主线程保持界面响应。 |
实战案例:多线程下载器
下面是一个简单的多线程网页内容下载器,对比单线程和多线程的执行时间。
import threading
import requests
import time
# 模拟要下载的URL列表
urls = [f"http://example.com/page{i}" for i in range(1, 11)]
def download_content(url):
"""下载单个URL的内容"""
try:
response = requests.get(url, timeout=5)
# print(f"成功下载: {url}, 内容长度: {len(response.text)}")
# 实际应用中,这里可以保存文件
except requests.RequestException as e:
print(f"下载失败 {url}: {e}")
# --- 单线程版本 ---
def single_thread_download():
start_time = time.time()
for url in urls:
download_content(url)
end_time = time.time()
print(f"单线程总耗时: {end_time - start_time:.2f} 秒")
# --- 多线程版本 ---
def multi_thread_download():
start_time = time.time()
threads = []
for url in urls:
# 为每个URL创建一个线程
thread = threading.Thread(target=download_content, args=(url,))
threads.append(thread)
thread.start() # 启动线程
# 等待所有线程完成
for thread in threads:
thread.join()
end_time = time.time()
print(f"多线程总耗时: {end_time - start_time:.2f} 秒")
if __name__ == "__main__":
print("--- 开始单线程下载 ---")
single_thread_download()
print("\n--- 开始多线程下载 ---")
multi_thread_download()
运行结果(类似):
--- 开始单线程下载 ---
单线程总耗时: 5.12 秒
--- 开始多线程下载 ---
多线程总耗时: 0.85 秒
可以看到,对于这种 I/O 密集型任务,多线程的效率远高于单线程。
- 多线程 是 Python 实现并发编程的重要工具,核心模块是
threading。 - 它最适合处理 I/O 密集型 任务,能显著提高程序效率。
- 进程 是资源分配的单位,线程 是 CPU 调度的单位,线程共享进程资源,开销小,但需要处理线程安全问题。
- 锁 (
Lock) 和 队列 (Queue) 是保证多线程程序正确性和可协作性的关键工具。 - GIL (全局解释器锁) 是 Python 多线程的“枷锁”,它限制了 Python 在 CPU 密集型任务上的并行能力,对于这类任务,应考虑使用
multiprocessing模块。
希望这个详细的解释能帮助你彻底理解 Python 的多线程!
