前言
在 Python 并发编程领域,进程(Process)、线程(Thread)与协程(Coroutine)是三种核心的任务调度方案。它们分别对应操作系统内核态的进程调度、内核态的线程调度与用户态的协程调度,在资源占用、切换效率、适用场景上存在本质差异。本文将从底层原理出发,结合 Python 实战案例,系统解析进程、线程与协程的实现机制、使用方法及选型策略,帮助开发者构建清晰的并发编程知识体系。
第一章 进程:操作系统的基础执行单元
1.1 进程的本质与核心特性
进程是操作系统进行资源分配和调度的基本单位,是程序在计算机中的一次执行过程。从底层视角看,一个进程包含以下核心组成部分:
- 进程控制块(PCB):存储进程的唯一标识(PID)、状态(运行 / 就绪 / 阻塞)、优先级、程序计数器(下一条待执行指令地址)等关键信息;
- 地址空间:包含代码段(程序指令)、数据段(全局变量 / 静态变量)、堆(动态内存分配区域)、栈(函数调用栈),且进程间地址空间完全隔离;
- 系统资源:如打开的文件描述符、网络连接、CPU 时间片、内存页等,由操作系统内核统一分配。
进程的核心特性决定了其在并发编程中的角色:
- 独立性:进程拥有独立的地址空间和资源,一个进程崩溃不会影响其他进程(除非通过进程间通信传递错误状态);
- 重量级:创建进程需分配 PCB、地址空间、资源,切换进程需保存 / 恢复 CPU 上下文(寄存器、程序计数器等),开销较大;
- 内核态调度:进程的创建、销毁、切换由操作系统内核管理,开发者通过 API 间接控制。
1.2 Python 中进程的实现与使用
Python 通过multiprocessing模块实现多进程编程,该模块封装了操作系统的fork(Linux)、CreateProcess(Windows)等系统调用,提供跨平台的进程管理能力。此外,subprocess模块用于创建子进程执行外部命令,os.fork(仅 Linux)可直接调用系统级进程创建接口。
1.2.1 基础进程创建:Process类
multiprocessing.Process是创建进程的核心类,其使用流程与threading.Thread(线程)类似,但底层实现完全不同(进程拥有独立 Python 解释器)。
import multiprocessing
import time
# 子进程执行的任务函数
def task(name, sleep_time):
print(f"子进程 {name}(PID: {multiprocessing.current_process().pid})启动")
time.sleep(sleep_time) # 模拟任务耗时
print(f"子进程 {name} 完成")
return f"{name} 结果" # 子进程返回值需通过Queue/ Pipe传递
if __name__ == "__main__":
# 1. 创建进程对象(未启动)
# target:子进程执行的函数;args:函数参数(元组);name:进程名
p1 = multiprocessing.Process(
target=task,
args=("任务A", 2),
name="Process-A"
)
p2 = multiprocessing.Process(
target=task,
args=("任务B", 3),
name="Process-B"
)
# 2. 启动进程(内核分配资源,创建独立地址空间)
start_time = time.time()
p1.start()
p2.start()
print(f"主进程(PID: {multiprocessing.current_process().pid})等待子进程完成")
# 3. 等待子进程结束(阻塞主进程,避免主进程提前退出)
p1.join()
p2.join()
# 4. 进程状态与退出码
print(f"子进程A是否存活:{p1.is_alive()}") # False
print(f"子进程B退出码:{p2.exitcode}") # 0(正常退出),非0表示异常
print(f"总耗时:{time.time() - start_time:.2f}秒") # 约3秒(并发执行)
关键说明:
- if name == "main": 必须添加:Windows 系统中,multiprocessing通过 "导入主模块" 创建子进程,若无此判断,子进程会重复执行主模块代码,导致无限递归创建进程;
- 进程启动流程:start()调用后,操作系统内核会复制主进程的地址空间(写时复制,Copy-On-Write),创建新的 PCB,将子进程加入就绪队列,等待 CPU 调度;
- 执行结果:子进程 A(2 秒)与 B(3 秒)并发执行,总耗时接近 3 秒,体现多进程的并行能力(若 CPU 核心数≥2,可真正并行;核心数 = 1 时,通过 CPU 时间片切换实现并发)。
1.2.2 进程间通信(IPC):解决数据隔离问题
由于进程地址空间独立,进程间无法直接共享内存,需通过操作系统提供的 IPC 机制实现数据交互。Python multiprocessing模块提供了 4 种常用 IPC 方案:
(1)队列(Queue):安全的多进程数据传递
基于管道(Pipe)和锁实现,支持多生产者 - 多消费者模式,是最常用的 IPC 方式。
import multiprocessing
def producer(queue):
"""生产者:向队列中放入数据"""
for i in range(5):
data = f"数据{i}"
queue.put(data)
print(f"生产者放入:{data}")
def consumer(queue):
"""消费者:从队列中获取数据"""
while True:
data = queue.get() # 若队列为空,阻塞等待
print(f"消费者获取:{data}")
queue.task_done() # 通知队列该任务已完成
if __name__ == "__main__":
# 创建队列(maxsize:队列最大容量,0表示无限)
queue = multiprocessing.Queue(maxsize=3)
# 创建生产者和消费者进程
p_producer = multiprocessing.Process(target=producer, args=(queue,))
p_consumer = multiprocessing.Process(target=consumer, args=(queue,), daemon=True)
# daemon=True:设置为守护进程,主进程退出时自动终止(避免消费者无限阻塞)
p_producer.start()
p_consumer.start()
p_producer.join() # 等待生产者完成所有数据放入
queue.join() # 等待队列中所有数据被消费
print("所有数据处理完成")
(2)管道(Pipe):双向 / 单向数据流
基于操作系统管道实现,适用于两个进程间的高效通信,支持双向传输(默认)或单向传输。
import multiprocessing
def send_data(pipe):
"""发送端:通过管道发送数据"""
pipe.send("Hello from send process")
print("发送端收到响应:", pipe.recv()) # 接收来自接收端的响应
pipe.close()
def recv_data(pipe):
"""接收端:通过管道接收数据"""
data = pipe.recv() # 阻塞等待数据
print("接收端收到数据:", data)
pipe.send("Response from recv process") # 发送响应
pipe.close()
if __name__ == "__main__":
# 创建管道:返回两个连接对象(conn1, conn2),默认双向通信
conn1, conn2 = multiprocessing.Pipe(duplex=True)
# 发送端使用conn1,接收端使用conn2
p_send = multiprocessing.Process(target=send_data, args=(conn1,))
p_recv = multiprocessing.Process(target=recv_data, args=(conn2,))
p_send.start()
p_recv.start()
p_send.join()
p_recv.join()
(3)共享内存(Array/Value):高效共享数据
通过操作系统共享内存区域实现数据共享,避免数据拷贝(队列 / 管道需拷贝数据),适用于高频数据交互场景。
import multiprocessing
def update_shared_data(shared_num, shared_array, lock):
"""更新共享数据:需加锁保证线程安全"""
with lock: # 共享内存无默认锁,需手动加锁避免竞态条件
shared_num.value += 1 # Value对象通过.value访问值
shared_array[0] += 1 # Array对象通过索引访问元素
print(f"共享数值:{shared_num.value},共享数组第一个元素:{shared_array[0]}")
if __name__ == "__main__":
# 创建共享内存对象
# Value(类型码, 初始值):类型码 'i' 表示int,'d'表示float
shared_num = multiprocessing.Value('i', 0)
# Array(类型码, 长度/初始值):创建固定长度的共享数组
shared_array = multiprocessing.Array('i', [0, 1, 2])
# 创建锁:保证共享数据操作的原子性
lock = multiprocessing.Lock()
# 启动3个进程同时更新共享数据
processes = [
multiprocessing.Process(
target=update_shared_data,
args=(shared_num, shared_array, lock)
) for _ in range(3)
]
for p in processes:
p.start()
for p in processes:
p.join()
# 主进程访问共享数据
print(f"最终共享数值:{shared_num.value}") # 3
print(f"最终共享数组:{list(shared_array)}") # [3,1,2]
(4)管理器(Manager):复杂数据结构共享
通过网络套接字实现跨进程共享,支持列表、字典、集合等复杂数据结构,适用于多进程共享复杂状态的场景(如配置信息、任务队列)。
import multiprocessing
def update_shared_dict(shared_dict, key, value):
"""更新共享字典"""
shared_dict[key] = value
print(f"进程 {multiprocessing.current_process().name} 更新字典:{shared_dict}")
if __name__ == "__main__":
# 创建管理器:管理共享对象的生命周期
with multiprocessing.Manager() as manager:
# 创建共享字典
shared_dict = manager.dict({"a": 1, "b": 2})
# 启动两个进程更新共享字典
p1 = multiprocessing.Process(
target=update_shared_dict,
args=(shared_dict, "a", 100),
name="Process-1"
)
p2 = multiprocessing.Process(
target=update_shared_dict,
args=(shared_dict, "c", 300),
name="Process-2"
)
p1.start()
p2.start()
p1.join()
p2.join()
# 主进程访问更新后的共享字典
print("最终共享字典:", dict(shared_dict)) # {'a': 100, 'b': 2, 'c': 300}
1.2.3 进程池(Pool):高效管理进程资源
当需要创建大量进程时(如 100 个任务),直接创建 100 个进程会导致资源浪费(每个进程占用内存、CPU)和调度开销。进程池通过预先创建固定数量的进程,循环复用进程处理任务,避免频繁创建 / 销毁进程的开销。
import multiprocessing
import time
def task(num):
"""任务函数:处理数字并返回结果"""
print(f"进程 {multiprocessing.current_process().pid} 处理任务:{num}")
time.sleep(1)
return num * 2
if __name__ == "__main__":
start_time = time.time()
# 1. 创建进程池:指定进程数量(通常为CPU核心数的1-2倍)
# multiprocessing.cpu_count() 获取CPU核心数
pool = multiprocessing.Pool(processes=multiprocessing.cpu_count())
# 2. 提交任务到进程池
# 方式1:map(批量提交任务,返回结果列表,按任务顺序排列)
tasks = [1, 2, 3, 4, 5]
results = pool.map(task, tasks)
print("map结果:", results) # [2,4,6,8,10]
# 方式2:apply_async(异步提交单个任务,返回AsyncResult对象,需调用get()获取结果)
async_result = pool.apply_async(task, args=(6,))
print("apply_async结果:", async_result.get()) # 12(阻塞等待结果)
# 3. 关闭进程池:不再接受新任务
pool.close()
# 4. 等待所有任务完成
pool.join()
print(f"总耗时:{time.time() - start_time:.2f}秒") # 约2秒(5个任务用4核CPU:2轮处理)
进程池核心方法:
- map(func, iterable):批量提交任务,将iterable中的每个元素作为参数传入func,返回结果列表(阻塞直到所有任务完成);
- apply_async(func, args):异步提交单个任务,返回AsyncResult对象,通过get(timeout)获取结果(可设置超时);
- close():关闭进程池,禁止提交新任务;
- terminate():立即终止所有进程(强制结束,可能导致任务未完成);
- join():等待所有进程完成任务(必须在close()或terminate()后调用)。
1.3 Python 多进程的局限性
尽管多进程能充分利用多核 CPU,解决 Python 全局解释器锁(GIL)的限制,但也存在明显局限性:
-
资源开销大:每个进程占用独立的内存空间(Python 进程默认占用数十 MB 内存),创建 / 切换进程需内核参与,开销远高于线程和协程;
-
IPC 复杂度高:进程间数据隔离,需通过队列、管道等机制传递数据,增加代码复杂度,且数据传递存在拷贝开销(共享内存除外);
-
不适合 I/O 密集型任务:对于网络请求、文件读写等 I/O 密集型任务,进程的创建 / 切换开销远大于 I/O 等待时间,性价比低(协程更适合);
-
GIL 无关但有其他限制:多进程避开了 GIL(每个进程有独立 GIL),但在 Windows 系统中,multiprocessing不支持fork,部分 C 扩展库可能存在兼容性问题。
第二章 线程:进程内的轻量级执行单元
2.1 线程的本质与核心特性
线程(Thread)是进程内的轻量级执行单元,是操作系统进行 CPU 调度的基本单位(现代操作系统以线程为调度粒度)。一个进程可包含多个线程,所有线程共享进程的地址空间和系统资源(如内存、文件描述符),但拥有独立的栈空间和程序计数器。
线程的核心特性可概括为 "轻量共享":
- 轻量级资源:线程无需独立地址空间,仅占用独立的栈空间(通常为几 MB),创建 / 销毁开销远低于进程(约为进程的 1/100-1/10);
- 轻量级切换:线程切换仅需保存 / 恢复线程的私有上下文(栈指针、程序计数器),无需切换地址空间,开销约为进程切换的 1/10-1/5;
- 资源共享:同一进程内的线程共享进程的代码段、数据段、堆空间及系统资源,无需 IPC 机制,数据交互效率高;
- 内核态调度:线程与进程一样由操作系统内核调度,支持抢占式调度(操作系统可强制剥夺线程的 CPU 使用权);
- 依赖性:线程依赖于进程存在,若进程退出,其所有线程会被强制终止;单个线程崩溃可能导致整个进程崩溃(共享内存空间)。
从操作系统实现角度,线程分为两种类型:
-
用户级线程(ULT):由用户程序管理,内核无法感知,切换效率高但无法利用多核;
-
内核级线程(KLT):由内核管理,可利用多核但切换开销较高;
-
Python 线程的实现:Python 的threading模块基于内核级线程实现,但受全局解释器锁(GIL)限制,无法真正利用多核 CPU 进行并行计算。
2.2 Python 中线程的实现与使用
Python 通过threading模块实现多线程编程,该模块封装了操作系统的内核级线程 API(如 Linux 的pthread、Windows 的CreateThread),提供简洁的线程创建、管理接口。此外,_thread模块是更底层的线程接口,功能简单且不推荐日常使用。
2.2.1 基础线程创建:Thread类
threading.Thread是创建线程的核心类,通过指定目标函数和参数,可快速创建并启动线程。
import threading
import time
# 线程执行的任务函数
def task(name, sleep_time):
"""
线程任务函数:模拟耗时操作,返回任务执行结果
Args:
name (str): 任务名称,用于标识当前线程处理的任务
sleep_time (int/float): 模拟任务耗时的时间(单位:秒)
Returns:
str: 任务执行完成后的结果提示
"""
# 打印线程启动信息,包含任务名和线程唯一标识(TID)
print(f"线程 {name}(TID: {threading.get_ident()})启动")
# 模拟耗时操作:此时线程会释放GIL,允许其他线程获取CPU执行权
time.sleep(sleep_time)
# 打印线程完成信息
print(f"线程 {name} 完成")
# 返回任务结果(原生threading.Thread无法直接获取,需通过队列/全局变量等方式传递)
return f"{name} 结果"
if __name__ == "__main__":
# 1. 创建线程对象(此时线程未启动,仅初始化配置)
# target:指定线程要执行的任务函数
# args:传递给任务函数的参数(元组类型,参数顺序与函数定义一致)
# name:设置线程名称,便于调试和状态查询
t1 = threading.Thread(
target=task,
args=("任务A", 2),
name="Thread-A"
)
t2 = threading.Thread(
target=task,
args=("任务B", 3),
name="Thread-B"
)
# 2. 启动线程:将线程提交给操作系统内核,加入就绪队列等待CPU调度
start_time = time.time() # 记录线程启动前的时间,用于计算总耗时
t1.start()
t2.start()
# 打印主线程状态:主线程继续执行,等待子线程完成
print(f"主线程(TID: {threading.get_ident()})等待子线程完成")
# 3. 阻塞主线程,等待子线程执行结束(避免主线程提前退出导致子线程被强制终止)
t1.join() # 等待线程t1(任务A)完成
t2.join() # 等待线程t2(任务B)完成
# 4. 线程状态查询与结果展示
print(f"线程A是否存活:{t1.is_alive()}") # 输出False(子线程已执行结束)
print(f"线程B名称:{t2.name}") # 输出Thread-B(获取线程预设名称)
print(f"总耗时:{time.time() - start_time:.2f}秒") # 约3秒(并发执行,取耗时最长的子线程时间)
关键说明:
- 线程标识(TID):通过threading.get_ident()获取线程的唯一标识(与操作系统的线程 ID 对应),区别于进程的 PID;
- GIL 释放时机:在time.sleep()、I/O 操作(如网络请求、文件读写)等阻塞操作时,线程会主动释放 GIL,其他线程可获取 CPU 执行权;
- 返回值获取:Thread类无法直接获取线程的返回值,需通过queue.Queue、全局变量或concurrent.futures.ThreadPoolExecutor的submit()方法获取。
2.2.2 线程间数据共享与线程安全
由于同一进程内的线程共享内存空间,线程间可直接通过全局变量、函数参数等方式共享数据,但需注意线程安全问题------ 当多个线程同时修改共享数据时,可能导致数据不一致(竞态条件,Race Condition)。
(1)线程安全问题示例
import threading
import time
# 共享变量:多个线程会同时修改该变量
count = 0
def increment():
"""
线程任务函数:对共享变量count进行10000次累加操作
注意:该函数存在线程安全问题,因为count += 1是非原子操作
"""
# 声明使用全局变量count(若不声明,函数内会视为局部变量,修改无效)
global count
# 循环10000次,每次对count进行累加
for _ in range(10000):
# 关键风险点:count += 1 本质是3步非原子操作
# 1. 读取当前count的值到内存临时变量
# 2. 对临时变量进行+1运算
# 3. 将运算结果写回count变量
# 多线程并发时,步骤可能交叉执行,导致结果小于预期
count += 1
if __name__ == "__main__":
# 1. 打印初始状态:确认共享变量初始值为0
print(f"共享变量初始值:{count}")
print("=" * 60)
# 2. 创建2个线程,目标任务均为increment(同时修改共享变量count)
# 线程1:负责对count累加10000次
t1 = threading.Thread(
target=increment,
name="Increment-Thread-1" # 设置线程名,便于调试追踪
)
# 线程2:负责对count累加10000次
t2 = threading.Thread(
target=increment,
name="Increment-Thread-2"
)
# 3. 启动线程:2个线程同时进入就绪状态,等待CPU调度执行
start_time = time.time()
t1.start()
t2.start()
print(f"线程启动完成:{t1.name} 和 {t2.name} 已启动,开始并发修改共享变量")
# 4. 阻塞主线程:等待2个线程全部执行结束,避免主线程提前打印结果
t1.join()
t2.join()
end_time = time.time()
# 5. 打印最终结果:展示线程安全问题(实际结果通常小于预期的20000)
print("=" * 60)
print(f"线程执行完成(总耗时:{end_time - start_time:.6f}秒)")
print(f"共享变量预期值:20000(2个线程各累加10000次)")
print(f"共享变量实际值:{count}(因非原子操作导致结果偏差)")
print("提示:需通过互斥锁(threading.Lock)保证count += 1操作的原子性,解决线程安全问题")
问题原因:count += 1并非原子操作,当两个线程同时执行时,可能出现 "读取 - 累加 - 写入" 的交叉执行,导致部分累加操作失效。
(2)线程安全解决方案:锁机制
Python 提供多种锁机制,用于保证共享数据操作的原子性,避免竞态条件:
① 互斥锁(Lock):最基础的锁
threading.Lock是最常用的互斥锁,通过acquire()获取锁、release()释放锁,保证同一时间只有一个线程执行临界区代码。
import threading
import time
# 共享变量:多个线程需并发修改的全局变量
count = 0
# 创建互斥锁(threading.Lock):保证同一时间只有一个线程进入临界区
# 作用:解决多线程对共享变量的竞争问题,避免竞态条件
lock = threading.Lock()
def increment():
"""
线程任务函数:对共享变量count进行10000次累加操作
优化点:通过互斥锁保护临界区(count += 1),确保操作原子性,解决线程安全问题
"""
# 声明使用全局变量count(不声明则视为函数内局部变量,修改无效)
global count
# 循环10000次,每次累加前通过锁保护操作
for _ in range(10000):
# 1. 获取互斥锁:若锁已被其他线程占用,当前线程会阻塞等待
# 直到持有锁的线程释放,当前线程才能获取锁进入临界区
lock.acquire()
try:
# 2. 临界区代码:必须保证原子性的操作(此处为count += 1)
# 锁的保护下,同一时间只有一个线程能执行该代码块
count += 1
finally:
# 3. 释放互斥锁:无论临界区代码是否抛出异常,都必须释放锁
# 避免因异常导致锁未释放,进而引发死锁(其他线程永远无法获取锁)
lock.release()
if __name__ == "__main__":
# 1. 打印初始状态:确认共享变量初始值为0
print(f"共享变量初始值:{count}")
print("=" * 60)
# 2. 创建2个线程,目标任务均为increment(并发修改共享变量count)
# 线程1:负责对count累加10000次(受锁保护)
t1 = threading.Thread(
target=increment,
name="Increment-Thread-1" # 设置线程名,便于调试和日志追踪
)
# 线程2:负责对count累加10000次(受锁保护)
t2 = threading.Thread(
target=increment,
name="Increment-Thread-2"
)
# 3. 启动线程:2个线程进入就绪状态,等待CPU调度(获取锁后执行临界区)
start_time = time.time()
t1.start()
t2.start()
print(f"线程启动完成:{t1.name} 和 {t2.name} 已启动(互斥锁保护共享变量修改)")
# 4. 阻塞主线程:等待2个线程全部执行结束,避免主线程提前打印结果
t1.join()
t2.join()
end_time = time.time()
# 5. 打印最终结果:锁保护下,结果符合预期(20000)
print("=" * 60)
print(f"线程执行完成(总耗时:{end_time - start_time:.6f}秒)")
print(f"共享变量预期值:20000(2个线程各累加10000次)")
print(f"共享变量实际值:{count}(互斥锁保证操作原子性,结果正确)")
print("核心原理:互斥锁将非原子的count += 1转为'获取锁→执行→释放锁'的原子流程,避免多线程操作交叉")
推荐用法:使用with语句自动管理锁的获取与释放,避免手动调用release()导致的死锁风险:
def increment():
global count
for _ in range(10000):
with lock: # 自动acquire()和release()
count += 1
② 可重入锁(RLock):支持嵌套获取
threading.RLock(Reentrant Lock)允许同一线程多次获取锁(避免嵌套锁导致的死锁),适用于临界区代码存在嵌套调用的场景。
import threading
rlock = threading.RLock()
def inner():
with rlock:
print("执行inner函数(已获取RLock)")
def outer():
with rlock:
print("执行outer函数(已获取RLock)")
inner() # 嵌套调用,同一线程可再次获取RLock
if __name__ == "__main__":
t = threading.Thread(target=outer)
t.start()
t.join()
③ 信号量(Semaphore):控制并发数量
threading.Semaphore通过计数器控制同时访问资源的线程数量,适用于限制资源的并发访问(如限制同时发起的网络请求数)。
import threading
import time
# 信号量:允许最多2个线程同时访问资源(计数器初始值为2)
semaphore = threading.Semaphore(2)
def task(name):
# 获取信号量:计数器-1;若计数器为0,线程阻塞等待
with semaphore:
print(f"线程 {name} 开始访问资源")
time.sleep(2) # 模拟资源占用耗时(如I/O操作、计算)
print(f"线程 {name} 结束访问资源")
# 退出with块自动释放信号量:计数器+1
if __name__ == "__main__":
# 创建5个线程,竞争访问资源(最多2个同时执行)
threads = [threading.Thread(target=task, args=(f"Thread-{i}",)) for i in range(5)]
# 启动所有线程
for t in threads:
t.start()
# 等待所有线程执行完成
for t in threads:
t.join()
2.2.3 线程池(ThreadPoolExecutor):高效管理线程资源
当需要创建大量线程时(如 1000 个任务),直接创建 1000 个线程会导致线程调度开销增大、内存占用过高(每个线程栈空间约 1-8MB)。线程池通过预先创建固定数量的线程,循环复用线程处理任务,避免频繁创建 / 销毁线程的开销。
Python 通过concurrent.futures.ThreadPoolExecutor实现线程池,该模块简化了线程池的创建和任务提交,支持获取任务返回值、超时控制等功能。
import concurrent.futures
import threading
import time
# 线程任务:处理数字并返回结果(I/O密集型模拟)
def task(num):
"""
线程池任务函数:接收数字并返回其2倍值
Args:
num (int): 待处理的数字
Returns:
int: 处理结果(num * 2)
"""
print(f"线程 {threading.get_ident()} 处理任务:{num}")
time.sleep(1) # 模拟I/O耗时操作(如网络请求、文件读写)
return num * 2
if __name__ == "__main__":
start_time = time.time()
# 创建线程池(with语句自动管理生命周期:启动→执行→关闭)
# max_workers:线程池最大线程数(I/O密集型任务可设为CPU核心数的5-10倍)
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
# 方式1:map 批量提交任务(按传入序列顺序返回结果)
tasks = [1, 2, 3, 4, 5]
results_map = executor.map(task, tasks)
print("map 批量结果:", list(results_map)) # 输出:[2, 4, 6, 8, 10]
# 方式2:submit 异步提交单个任务(返回Future对象,需主动获取结果)
future = executor.submit(task, 6)
# result() 会阻塞等待任务完成,可通过 timeout 参数设置超时(如 timeout=2)
results_submit = future.result()
print("submit 单个结果:", results_submit) # 输出:12
# with 语句块结束后,线程池自动关闭,无需手动调用 shutdown()
print(f"总耗时:{time.time() - start_time:.2f}秒") # 约2秒(4线程处理6个任务,分2轮)
线程池核心优势:
- 自动资源管理:with语句自动创建和关闭线程池,避免资源泄漏;
- 返回值获取:通过map()或Future.result()直接获取任务返回值,无需手动管理队列;
- 超时控制:Future.result(timeout)可设置超时时间,避免无限等待;
- 异常处理:任务执行过程中抛出的异常会在调用result()时重新抛出,便于统一处理。
2.3 全局解释器锁(GIL):Python 线程的 "痛点"
全局解释器锁(Global Interpreter Lock,GIL)是 Python 解释器(CPython)的核心机制,本质是一把互斥锁,用于保证同一时间只有一个线程执行 Python 字节码。GIL 的存在导致 Python 多线程无法真正利用多核 CPU 进行并行计算,是 Python 线程的最大局限性。
2.3.1 GIL 的工作原理
GIL 的核心逻辑可概括为 "切换与释放":
-
初始状态:主线程启动时获取 GIL,执行 Python 字节码;
-
线程切换触发:
-
当线程执行到指定字节码数量(默认 100 条)时,解释器会主动释放 GIL,触发线程调度;
-
当线程执行 I/O 操作(如time.sleep()、网络请求、文件读写)时,会主动释放 GIL,等待 I/O 完成;
-
-
GIL 竞争:所有处于就绪状态的线程会竞争 GIL,获取 GIL 的线程继续执行,未获取的线程进入阻塞状态。
2.3.2 GIL 对 Python 线程的影响
GIL 的存在导致 Python 多线程在不同类型任务中的表现差异巨大:
(1)对 CPU 密集型任务:多线程无优势
CPU 密集型任务(如大量计算、数据处理)中,线程很少触发 I/O 操作,GIL 释放机会少,多线程本质上是 "串行执行",无法利用多核 CPU。此时多线程的执行效率甚至低于单线程(线程切换开销)。
示例:CPU 密集型任务的多线程与单线程对比
import threading
import time
# CPU密集型任务:计算1到n的累加和(无I/O操作,持续占用CPU)
def cpu_intensive_task(n):
result = 0
for i in range(n):
result += i
return result
if __name__ == "__main__":
n = 10**8 # 大数字,确保任务耗时足够长,凸显GIL限制
print("=" * 60)
# 1. 单线程执行CPU密集型任务
start_time = time.time()
cpu_intensive_task(n)
single_time = time.time() - start_time
print(f"单线程耗时:{single_time:.2f}秒")
print("=" * 60)
# 2. 双线程执行CPU密集型任务(拆分任务为2份,模拟并行尝试)
start_time = time.time()
t1 = threading.Thread(target=cpu_intensive_task, args=(n // 2,))
t2 = threading.Thread(target=cpu_intensive_task, args=(n // 2,))
t1.start()
t2.start()
t1.join()
t2.join()
multi_time = time.time() - start_time
print(f"双线程耗时:{multi_time:.2f}秒")
print("=" * 60)
# 核心结论:GIL限制下,CPU密集型任务多线程无并行优势
print(f"结论:双线程耗时 ≈ 单线程耗时(甚至更长)")
print("原因:CPython的GIL强制同一时间仅一个线程执行Python字节码,")
print("多线程CPU密集型任务仅能并发(交替执行),无法并行(同时执行),")
print("且线程切换存在额外开销,导致效率无提升甚至下降。")
(2)对 I/O 密集型任务:多线程有优势
I/O 密集型任务(如网络请求、文件读写)中,线程在 I/O 等待期间会释放 GIL,其他线程可获取 CPU 执行,因此多线程能显著提升并发效率(减少 I/O 等待时间)。
示例:I/O 密集型任务的多线程优势
import threading
import time
import requests
# I/O密集型任务:发起网络请求,返回响应内容长度
def io_intensive_task(url):
"""
发起HTTP GET请求,获取目标URL的响应内容长度
Args:
url (str): 目标请求URL
Returns:
int: 响应文本内容的字符长度
"""
response = requests.get(url)
return len(response.text)
if __name__ == "__main__":
# 待请求的URL列表(5个主流网站,模拟I/O密集场景)
urls = [
"https://www.baidu.com",
"https://www.github.com",
"https://www.python.org",
"https://www.zhihu.com",
"https://www.csdn.net"
]
print("=" * 60)
# 1. 单线程执行:串行发起网络请求
start_time = time.time()
for url in urls:
io_intensive_task(url)
single_time = time.time() - start_time
print(f"单线程耗时:{single_time:.2f}秒")
print("=" * 60)
# 2. 多线程执行:并行发起网络请求
start_time = time.time()
# 创建线程列表:每个URL对应一个线程
threads = [
threading.Thread(target=io_intensive_task, args=(url,))
for url in urls
]
# 启动所有线程
for t in threads:
t.start()
# 等待所有线程执行完成(阻塞主线程)
for t in threads:
t.join()
multi_time = time.time() - start_time
print(f"多线程耗时:{multi_time:.2f}秒")
print("=" * 60)
# 核心结论:I/O密集型任务多线程效率显著提升
print(f"效率提升:{(single_time - multi_time) / single_time * 100:.1f}%")
print("原因:网络请求的核心耗时是'等待响应'(I/O阻塞),")
print("此时线程会释放GIL,其他线程可继续执行,充分利用CPU空闲时间,")
print("避免单线程时的'等待浪费',实现并发加速。")
2.3.3 规避 GIL 限制的方案
尽管 GIL 限制了 Python 多线程的并行能力,但可通过以下方案规避:
-
使用多进程:多进程中每个进程有独立的 Python 解释器和 GIL,可利用多核 CPU(multiprocessing模块);
-
使用协程:协程运行在单个线程中,通过用户态调度避免 GIL 切换开销,适合 I/O 密集型任务(asyncio模块);
-
使用 C 扩展:在 C 扩展模块中释放 GIL(如numpy、scipy等科学计算库),实现 CPU 密集型任务的并行;
-
使用其他 Python 解释器:Jython(基于 Java 虚拟机)、IronPython(基于.NET 框架)等解释器无 GIL,支持真正的多线程并行。
2.4 Python 多线程的局限性
除 GIL 外,Python 多线程还存在以下局限性:
-
线程安全问题:线程共享内存空间,需手动加锁保证线程安全,增加代码复杂度,且可能导致死锁;
-
资源占用较高:每个线程占用独立的栈空间(约 1-8MB),创建大量线程(如 10000 个)会导致内存溢出;
-
调试难度大:多线程的执行流程受内核调度影响,难以复现和调试线程安全问题(如死锁、竞态条件);
-
不适合超大规模并发:对于百万级并发(如高并发服务器),多线程的调度开销和内存占用过高,协程更适合。