并发编程的核心在于“同时处理多个任务”,常用于 网络请求加速、IO密集型任务提速、并行计算 等场景。下面是几个关键概念:
并发 vs 并行
| 概念 | 含义 | 场景举例 |
|---|---|---|
| 并发(Concurrency) | 多个任务在同一时间段交替进行(一个CPU核切换任务) | 单核 CPU 执行多任务 |
| 并行(Parallelism) | 多个任务在同一时刻同时进行(多个CPU核同时执行) | 多核 CPU 同时运行多个程序 |
进程 vs 线程
| 特性 | 进程 | 线程 |
|---|---|---|
| 定义 | 程序的独立运行单位 | 进程内的执行单元 |
| 内存空间 | 独立 | 共享进程内存 |
| 资源开销 | 大(启动慢) | 小(启动快) |
| 通信方式 | 进程间通信(IPC)较复杂 | 共享变量、易通信 |
| 稳定性 | 一个崩溃不影响其他进程 | 一个线程崩溃可能影响整个进程 |
| 调度 | 操作系统调度 | 操作系统调度 |
一、线程(Thread)
线程是进程中的一个执行单元,是 CPU 调度的最小单位。多个线程共享进程的资源(如内存、文件句柄)。
-
共享内存:同一个进程的线程之间共享堆、全局变量。
-
开销小、切换快:比进程更轻量,创建和销毁的代价低。
-
协作性强:线程间通信更快,但也更容易引发同步问题(如竞争条件)。
比如你在一个浏览器窗口里,同时播放视频、加载评论、更新弹幕,这些任务可能是由多个线程在一个进程中并行完成的。
1. 线程的基本操作
创建线程的两种方式
方法一:使用 threading.Thread(target=...)
import threading
def worker():
print("工作线程执行中")
# 创建线程
t = threading.Thread(target=worker)
t.start() # 启动线程
t.join() # 等待线程结束
方法二:继承 threading.Thread 类(更灵活)
class MyThread(threading.Thread):
def run(self): # 重写 run 方法
print("自定义线程执行中")
t = MyThread()
t.start()
t.join()
2. start() 和 run() 的区别与原理
| 方法 | 说明 |
|---|---|
start() | 通知系统创建一个新的线程,由系统调用 run() 方法(真正开了一个线程) |
run() | 是线程的具体逻辑函数,如果你直接调用它,就只是普通函数调用(不会创建新线程) |
示例对比:
import threading
import time
class MyThread(threading.Thread):
def run(self):
print(f"线程开始执行:{threading.current_thread().name}")
time.sleep(1)
print("线程执行完毕")
t = MyThread()
print("直接调用 run():")
t.run() # 不会创建新线程,只是普通函数调用
print("调用 start():")
t.start() # 真正开了一个新线程
t.join()
输出说明:
-
run():执行在主线程 -
start():开启新线程,由操作系统调度运行run()
3. daemon(守护线程)
守护线程(daemon thread)随主线程退出而退出。主线程结束后,非守护线程必须执行完,守护线程可直接终止。
非守护线程(用户线程)——主要工作线程 程序的“核心逻辑部分”。如:文件写入、数据下载等。
守护线程(后台线程)——辅助性工作线程 如日志记录、心跳检测、资源监控等,不是主功能。
import threading
import time
def daemon_worker():
print("守护线程开始")
time.sleep(5)
print("守护线程结束") # 不一定能执行到
t = threading.Thread(target=daemon_worker)
t.daemon = True # 设置为守护线程
t.start()
print("主线程结束") # 守护线程可能还没执行完
输出结果:
守护线程开始
主线程结束
(可能没有"守护线程结束")
注意:daemon 必须在 start() 之前设置!
4. join() 的作用
join() 会让主线程 等待该子线程执行完毕后再继续运行。
示例:
import threading
import time
def worker():
print("子线程开始")
time.sleep(2)
print("子线程结束")
t = threading.Thread(target=worker)
t.start()
t.join() # 等子线程结束再往下走
print("主线程结束")
如果没有 join(),主线程可能在线程结束前就退出了。
5. 完整示例
import threading
import time
class MyThread(threading.Thread):
def run(self):
print(f"[{self.name}] 开始")
time.sleep(2)
print(f"[{self.name}] 结束")
# 普通线程
t1 = MyThread(name="普通线程")
t1.start()
# 守护线程
t2 = MyThread(name="守护线程")
t2.daemon = True
t2.start()
# 主线程等待 t1 完成,但不等守护线程
t1.join()
print("主线程结束")
| 操作 | 说明 |
|---|---|
start() | 启动一个线程,由系统调用 run() |
run() | 重写此方法定义线程行为,直接调用不会创建线程 |
join() | 阻塞当前线程,直到目标线程执行完毕 |
daemon=True | 设置线程为守护线程,随主线程退出而退出 |
6. 线程间通信:Queue
线程之间安全地传递数据,用 queue.Queue,线程安全。
import queue
q = queue.Queue()
def producer():
for i in range(5):
q.put(i)
print(f"生产了 {i}")
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()
q.put(None) # 终止信号
t2.join()
二、进程(Process)
Python 中的 进程(Process) 是并发编程的一种方式,尤其适合 CPU 密集型任务。
- 进程是操作系统资源分配的基本单位,每个进程拥有独立的内存空间。
- Python 的多线程受限于 GIL(全局解释器锁),但多进程不受影响,能真正实现并行执行。
- 独立性强:进程之间相互独立,一个进程崩溃不会影响其他进程。
- 拥有完整资源:每个进程都拥有自己的堆栈、数据段、代码段。
- 创建成本高:启动一个新进程需要较多的资源(如内存、时间)。
- 程序是静态的代码,进程是程序的动态运行实例。
比如你打开两个浏览器,一个看视频,一个登录网页,这两个浏览器实例就是两个进程,互相独立运行。
1. Python 创建进程的方式
方法一:使用 multiprocessing.Process
from multiprocessing import Process
import time
def task(name):
print(f'{name} started')
time.sleep(2)
print(f'{name} finished')
if __name__ == '__main__':
p = Process(target=task, args=('进程1',))
p.start()
p.join()
print("主进程结束")
方法二:继承 multiprocessing.Process 类
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f'{self.name} 执行中')
time.sleep(1)
print(f'{self.name} 完成')
if __name__ == '__main__':
p = MyProcess("子进程")
p.start()
p.join()
2. 常用操作详解
start() vs run()
-
start():启动新进程,系统调用run()方法 -
run():定义进程逻辑,直接调用不会启动新进程
join()
-
阻塞主进程,直到子进程执行结束。
3. 进程间通信(IPC)
由于进程之间不共享内存,通信需要借助队列、管道、共享内存等机制:
使用 Queue 实现通信:
from multiprocessing import Process, Queue
def worker(q):
q.put("来自子进程的消息")
if __name__ == '__main__':
q = Queue()
p = Process(target=worker, args=(q,))
p.start()
print(q.get()) # 接收子进程传回来的数据
p.join()
4. 进程池(Pool)
适用于大量任务并发处理,自动管理多个进程。
from multiprocessing import Pool
import time
def task(n):
time.sleep(1)
return n * n
if __name__ == '__main__':
with Pool(4) as pool: # 创建包含4个进程的进程池
results = pool.map(task, [1, 2, 3, 4, 5])
print(results)
map 会自动将任务分配给多个进程并收集结果。
5. 守护进程(daemon)
from multiprocessing import Process
import time
def worker():
print("子进程启动")
time.sleep(3)
print("子进程结束")
if __name__ == '__main__':
p = Process(target=worker)
p.daemon = True # 设置为守护进程
p.start()
print("主进程结束") # 守护进程随主进程结束而终止
三、threading.local
threading.local() 是 Python 的一个特殊类,用来创建每个线程自己私有的变量空间。
每个线程对它进行读写,不会影响到别的线程。
1. 为什么需要 threading.local?
在多线程中,我们经常遇到全局变量被多个线程共享导致数据冲突的问题。
例子(共享全局变量):
import threading
var = {}
def worker(x):
var['value'] = x
print(f"{threading.current_thread().name}: {var['value']}")
for i in range(3):
t = threading.Thread(target=worker, args=(i,))
t.start()
你会看到输出是混乱的,因为线程间共享同一个 var。
用 threading.local() 解决上述问题,改写的代码如下:
import threading
# 创建线程局部变量容器
local_data = threading.local()
def worker(x):
# 为当前线程设置一个名为 value 的变量
local_data.value = x
# threading.current_thread(): 当前正在运行的线程对象
print(f"{threading.current_thread().name}: {local_data.value}")
# 启动3个线程,每个线程传入不同的参数 i
for i in range(3):
t = threading.Thread(target=worker, args=(i,))
t.start()
每个线程都有自己的 local_data.value,互不影响!
2. 底层原理简要说明
threading.local() 会在每个线程内部创建一份独立的字典,存储这个线程对应的数据。
内部相当于:
thread_id = get_current_thread_id()
data_store[thread_id] = {"value": x}
3. 实战应用场景举例
日志系统中的上下文变量:
在多线程环境下,每个线程都处理不同的用户请求,你可能希望每个日志条目都带上“用户ID”:
import threading
user_context = threading.local()
def set_user(user_id):
user_context.uid = user_id
def log(message):
print(f"[用户 {user_context.uid}] {message}")
def handle_request(uid):
set_user(uid)
log("请求开始")
threads = []
for uid in range(3):
t = threading.Thread(target=handle_request, args=(uid,))
threads.append(t)
t.start()
数据库连接管理器:
每个线程处理一个用户请求,不希望多个线程共用一个连接。
connections = threading.local()
def get_connection():
if not hasattr(connections, "conn"):
connections.conn = create_connection()
return connections.conn
| 注意点 | 说明 |
|---|---|
| 不共享 | 不同线程设置的属性互不影响 |
| 和线程生命周期绑定 | 线程结束后,对应的属性自动清除 |
| 不适合多进程 | threading.local() 只适用于多线程,不适用于 multiprocessing |
测试代码:
import threading
import time
local_var = threading.local()
def f():
local_var.val = threading.current_thread().name
time.sleep(1)
print(f"{threading.current_thread().name}: {local_var.val}")
threads = []
for i in range(5):
t = threading.Thread(target=f, name=f"线程-{i}")
threads.append(t)
t.start()
4.总结
| 项目 | 内容 |
|---|---|
| 类名 | threading.local() |
| 作用 | 创建线程私有的变量空间 |
| 每个线程访问的是 | 自己的副本,互不干扰 |
| 应用场景 | 日志上下文、数据库连接、线程用户数据等 |
四、threading.Event
Event 是一个线程同步原语,用于线程之间通过标志位进行通信。
可以理解成:一个可以被“设置”或“清除”的信号量(开关)。
| 方法 | 作用 |
|---|---|
set() | 把标志位设为 True,通知等待的线程 |
clear() | 把标志位设为 False |
is_set() | 检查标志位是否为 True |
wait() | 如果标志为 False,线程阻塞,直到 set() 被调用 |
1. 简单例子:等待一个“信号”
import threading
import time
event = threading.Event()
def worker():
print("线程开始执行,等待信号...")
event.wait() # 等待 set()
print("收到信号,线程继续执行")
t = threading.Thread(target=worker)
t.start()
time.sleep(2)
print("主线程2秒后发出信号")
event.set()
运行输出:
线程开始执行,等待信号...
主线程2秒后发出信号
收到信号,线程继续执行
2. 使用场景:线程控制 + 通信
主线程控制子线程启动/暂停:
event = threading.Event()
def worker():
print("子线程准备中...")
event.wait() # 等待“启动”信号
print("子线程开始干活")
event.clear()
t = threading.Thread(target=worker)
t.start()
time.sleep(3)
event.set() # 通知子线程可以开始
多线程等待统一信号(发布-订阅)
event = threading.Event()
def listener(name):
print(f"{name} 等待开门")
event.wait()
print(f"{name} 进门了")
for i in range(3):
threading.Thread(target=listener, args=(f"线程-{i}",)).start()
time.sleep(2)
print("门打开(发送信号)")
event.set()
实现暂停/继续功能(配合 clear())
event = threading.Event()
def runner():
while True:
event.wait() # 一直等着 resume 信号
print("线程正在运行")
time.sleep(1)
event.set() # 初始为允许运行
t = threading.Thread(target=runner)
t.start()
time.sleep(3)
print("暂停线程")
event.clear()
time.sleep(3)
print("恢复线程")
event.set()
-
event.wait()是阻塞的,直到你event.set()才继续。 -
一旦
set()被调用,所有调用wait()的线程都将继续执行。 -
如果你想再次阻塞线程,需要用
clear()重置。
3. 和锁(Lock)区别
| 对象 | 功能 | 适合场景 |
|---|---|---|
Lock | 用于互斥访问共享资源 | 多个线程访问同一个变量时 |
Event | 用于线程间通信、同步调度 | 控制线程启动、同步等待 |
4. 实战应用:爬虫控制系统中的“启动”信号
import threading
import time
event = threading.Event()
def spider_thread(name):
print(f"{name} 等待管理员下达启动命令...")
event.wait()
print(f"{name} 正在抓取网页...")
for i in range(5):
threading.Thread(target=spider_thread, args=(f"爬虫-{i}",)).start()
time.sleep(2)
print("管理员:全部启动")
event.set()
5. 总结
| 方法 | 用途 |
|---|---|
event.wait() | 阻塞线程直到事件被 set |
event.set() | 发出“允许执行”的信号 |
event.clear() | 将事件标志重置为 False,重新阻塞 |
event.is_set() | 检查当前事件状态(True/False) |
Event 本质上是一个线程之间的“通知机制”,就像红绿灯一样控制线程的执行。
五、threading.Lock
| 方法 | 说明 |
|---|---|
lock.acquire() | 手动加锁(阻塞) |
lock.release() | 手动解锁 |
lock.acquire(timeout=3) | 最多等待3秒获得锁 |
with lock: | 推荐写法(自动获取和释放锁) |
1. Lock 的使用与分析
threading.Lock 是最基本的互斥锁,用来防止多个线程同时访问共享资源。
用法一:手动 acquire() / release()
lock = threading.Lock()
lock.acquire()
try:
# 访问临界资源
finally:
lock.release()
用法二:推荐的 with 上下文语法
with lock:
# 安全访问共享资源
这相当于自动调用 acquire() 和 release(),即便中间出错也会释放。
如果不加锁会怎样?
假设有两个线程同时执行 count += 1,它其实分为:
1. 读取 count 到寄存器
2. 加 1
3. 写回 count
两个线程可能会这样交错执行,导致最终结果不是预期的。
实验:加锁 VS 不加锁
import threading
count = 0
lock = threading.Lock()
def add():
global count
for _ in range(100000):
with lock:
count += 1
threads = [threading.Thread(target=add) for _ in range(2)]
for t in threads: t.start()
for t in threads: t.join()
print("最终结果:", count) # 应该是 200000
不加锁时,结果通常是 17~19 万。
2. Lock 的原理(内部机制)
GIL ≠ Lock
Python 的 GIL(全局解释器锁)确实控制线程执行,但 GIL 只保护解释器本身,不保护你写的共享变量。
所以多线程写变量必须用 Lock 手动控制。
Lock 内部实现(C 层)
-
Python 的
threading.Lock实际上是thread.allocate_lock()实现的。 -
对不同系统底层封装了系统原生的互斥锁(如 POSIX Mutex)。
-
类似伪代码:
class Lock:
def acquire(self):
# 尝试获取锁,阻塞直到成功
def release(self):
# 释放锁,让下一个线程继续
死锁问题
死锁场景:两个线程拿住了对方想要的资源,彼此等待
lock1 = threading.Lock()
lock2 = threading.Lock()
def t1():
with lock1:
time.sleep(1)
with lock2: # 可能卡住
pass
def t2():
with lock2:
time.sleep(1)
with lock1: # 也卡住
pass
死锁示意图:
t1: 持有 lock1,等待 lock2
t2: 持有 lock2,等待 lock1
==> 永久等待,程序卡住不动
解决方法之一就是使用 可重入锁 RLock 或 设置锁获取超时。
3. RLock(可重入锁)
什么是 RLock?
-
RLock = Reentrant Lock(可重入锁)
-
允许同一个线程多次获得同一个锁,不会死锁。
普通 Lock 的问题:
lock = threading.Lock()
def outer():
with lock:
inner()
def inner():
with lock: # 死锁:自己套自己
print("Inner logic")
outer()
RLock 正确用法
lock = threading.RLock()
def outer():
with lock:
inner()
def inner():
with lock: # 允许同一线程重复加锁
print("Inner logic")
outer()
内部原理:
-
RLock 内部维护了一个锁计数器和持有锁的线程 ID。
-
第一次
acquire()后计数 +1; -
每次
release()计数 -1; -
只有计数归 0,才真正释放锁。
4. 实战案例:线程安全的计数器类
import threading
class SafeCounter:
def __init__(self):
self.count = 0
self.lock = threading.Lock()
def increment(self):
with self.lock:
self.count += 1
def get(self):
with self.lock:
return self.count
counter = SafeCounter()
def worker():
for _ in range(10000):
counter.increment()
threads = [threading.Thread(target=worker) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print("最终计数:", counter.get())
5. 总结
| 关键词 | 含义 |
|---|---|
| Lock | 多线程控制共享资源互斥访问的最基本工具 |
| acquire/release | 显式加锁与释放 |
| with lock | 推荐写法,防异常未释放 |
| 死锁 | 多锁交叉等待,需避免 |
| RLock | 可重入锁,解决递归调用时的锁问题 |
六、threading.Condition
Condition 是一个高级同步原语,允许多个线程基于某个状态进行等待和通知。
典型应用场景:一个线程等待资源准备好,另一个线程通知资源已就绪。
-
Condition是建立在Lock(或RLock)基础上的。 -
提供了两个核心机制:
-
wait():挂起当前线程,等待其他线程通知。 -
notify()/notify_all():唤醒一个 / 所有等待的线程。
-
1. 基本用法示例
场景:消费者等待商品,生产者生产后通知
import threading
import time
# product_ready: 共享变量,表示产品是否准备好,初始为 False
product_ready = False
# condition: 一个 条件变量(Condition Object),用于线程之间通信和等待唤醒。
condition = threading.Condition()
def consumer():
with condition:
print("消费者:等待产品")
while not product_ready:
condition.wait() # 阻塞,等待被唤醒
print("消费者:收到产品,开始消费")
def producer():
global product_ready
time.sleep(2) # 模拟生产耗时
with condition:
product_ready = True
print("生产者:产品已生产,通知消费者")
condition.notify() # 唤醒等待的消费者
threading.Thread(target=consumer).start()
threading.Thread(target=producer).start()
2. Condition 的方法解析
| 方法 | 说明 |
|---|---|
acquire() / release() | 底层锁的获取与释放(不常直接用) |
wait(timeout=None) | 当前线程进入“等待队列”直到被唤醒 |
notify(n=1) | 唤醒最多 n 个等待线程(默认1个) |
notify_all() | 唤醒所有等待线程 |
所有这些方法都必须在加锁状态下调用,否则会抛异常。
为什么 wait() 要放在循环里?
因为被唤醒后不代表条件一定成立(可能是被误唤、条件还没满足):
while not 条件:
condition.wait()
3. 底层原理
-
condition.wait()会:-
释放底层锁;
-
把当前线程加入等待队列;
-
阻塞直到被
notify()唤醒; -
再次尝试获取锁(被唤醒后不是立刻运行,要竞争锁);
-
-
condition.notify()会:-
将等待队列中的线程移出,并让它尝试重新获取锁;
-
不立即执行,要等持锁线程释放锁。
-
4. 实战案例:经典生产者消费者模型
import threading
import time
queue = []
MAX_SIZE = 5
condition = threading.Condition()
def producer():
while True:
with condition:
while len(queue) >= MAX_SIZE:
print("队列满,生产者等待")
condition.wait()
queue.append(1)
print("生产者:生产了一个产品,现在库存", len(queue))
condition.notify()
time.sleep(0.5)
def consumer():
while True:
with condition:
while not queue:
print("队列空,消费者等待")
condition.wait()
queue.pop()
print("消费者:消费了一个产品,现在库存", len(queue))
condition.notify()
time.sleep(1)
threading.Thread(target=producer).start()
threading.Thread(target=consumer).start()
Condition 与 Lock 的区别
| 比较点 | Lock | Condition |
|---|---|---|
| 功能 | 互斥访问资源 | 等待条件满足与唤醒 |
| 是否能通知 | 无通知机制 | wait() + notify() 实现通信 |
| 场景 | 单个共享资源同步 | 多线程协调、等待/通知、状态依赖场景 |
| 是否必须配合锁 | 本身就是锁 | 必须配合底层锁(可传入或内部自建) |
七、信号量(Semaphore)
信号量(Semaphore)是一种用于控制“允许同时访问资源的线程数量”的同步机制。
可以理解为一个“计数器锁”:它允许最多 N 个线程同时访问某个临界资源。
举例说明:
-
有 3 个数据库连接线程池,你最多只能允许 3 个线程同时使用。
-
超过的线程必须等待——这时候信号量就能起作用。
1. 基本语法和使用方式
from threading import Semaphore, Thread
import time
# 最多允许 3 个线程同时执行
sem = Semaphore(3)
def task(name):
print(f"{name} 想要进入")
with sem: # 等同于 acquire()/release()
print(f"{name} 进入了")
time.sleep(2)
print(f"{name} 离开了")
for i in range(5):
Thread(target=task, args=(f"线程{i}",)).start()
输出结果(简略):
线程0 想要进入
线程0 进入了
线程1 想要进入
线程1 进入了
线程2 想要进入
线程2 进入了
线程3 想要进入
线程4 想要进入
# 等 2 秒后...
线程0 离开了
线程3 进入了
线程1 离开了
线程4 进入了
2. 核心方法
| 方法 | 说明 |
|---|---|
Semaphore(n) | 创建一个初始值为 n 的信号量 |
acquire() | 获取信号量(计数 -1,必要时阻塞) |
release() | 释放信号量(计数 +1) |
with sem: 会自动调用 acquire() 和 release()。
3. 信号量的原理
内部维护一个计数器:
-
初始值为
n; -
每次调用
acquire(),计数器减 1;-
如果计数器 < 0,线程进入阻塞等待;
-
-
每次
release(),计数器加 1,唤醒等待线程。
4. 信号量 VS Lock 区别
| 比较点 | Lock | Semaphore(n) |
|---|---|---|
| 控制线程数 | 只允许 1 个线程 | 允许最多 n 个线程 |
| 使用场景 | 互斥访问资源 | 资源池、线程配额控制等 |
| 阻塞机制 | 有线程时阻塞等待 | 超过 n 个时阻塞等待 |
5. BoundedSemaphore
是信号量的子类,防止 release() 超过最大值。
from threading import BoundedSemaphore
# 最多允许 2 个线程
sem = BoundedSemaphore(2)
sem.acquire()
sem.release()
sem.release() # 报错:释放次数多于获取次数
适合场景:确保资源归还次数不能多于借出次数(比如数据库连接池)。
6. 实战场景:多线程爬虫限并发
import threading
import time
import random
sem = threading.Semaphore(5) # 最多 5 个并发线程
def download_page(url):
with sem:
print(f"下载 {url}")
time.sleep(random.uniform(1, 3))
print(f"完成 {url}")
urls = [f"http://example.com/page{i}" for i in range(20)]
threads = [threading.Thread(target=download_page, args=(url,)) for url in urls]
for t in threads: t.start()
for t in threads: t.join()
| 概念 | 内容 |
|---|---|
| 信号量 | 控制同时访问资源的线程数量 |
| 用法 | with Semaphore(n): 或手动 acquire() / release() |
| 应用场景 | 资源池管理、线程限流、连接控制 |
| 和 Lock 区别 | Lock 是一把“独占锁”,信号量是“计数锁” |
八、GIL
GIL(Global Interpreter Lock)是 Python 解释器在执行字节码时使用的全局锁,同一时间只允许一个线程执行 Python 代码。
注意:它是 CPython 解释器(最常用的 Python 实现)特有的机制,其他如 PyPy、Jython 不一定有 GIL。
GIL 是为了简化 CPython 的内存管理而引入的。
-
Python 中很多操作依赖于 引用计数(ref count) 管理内存。
-
如果多个线程同时修改一个对象的引用计数,没有加锁就会出错。
-
GIL 让 Python 在任意时刻只有一个线程执行解释器逻辑,这样可以避免加大量底层锁,提高安全性和实现效率。
GIL 是个互斥锁,具有以下特点:
-
只要是运行 Python 字节码(解释器层),就必须先获取 GIL。
-
Python 会在每个线程中定时地释放 GIL(默认每隔一定“字节码次数”)。
-
当 GIL 被释放后,其他线程才有机会获取它并执行。
-
GIL 不影响 IO 操作(比如网络请求、磁盘读写时会主动释放 GIL)。
1. GIL 对多线程的影响
CPU 密集型(CPU-bound)任务,指的是:
主要消耗 CPU 资源的任务,几乎不进行 I/O(例如磁盘、网络)操作。
常见的 CPU 密集型任务:
-
数值计算(如大循环、大量加减乘除)
-
图像处理(如像素遍历、滤波)
-
加密解密、压缩解压
-
人工智能模型推理
-
编译、哈希计算
I/O 密集型(Input/Output-bound)任务指的是:
程序大部分时间花在等待外部设备(网络、磁盘、数据库等)响应,而不是 CPU 运算。
示例:
| 操作 | 类型 | 是否I/O密集 |
|---|---|---|
time.sleep() | 模拟 | 是 |
requests.get() 请求网页 | 实际网络 I/O | 是 |
open('file.txt').read() | 磁盘 I/O | 是 |
| 数据库查询(如 MySQL) | 网络 I/O | 是 |
对比:
| 情况 | 表现 |
|---|---|
| CPU 密集型任务 | 多线程基本无效,效率反而更差 |
| IO 密集型任务 | 多线程有效,提高效率 |
CPU 密集型 示例(效果差)
import threading
def cpu_task():
count = 0
for _ in range(10**7):
count += 1
threads = [threading.Thread(target=cpu_task) for _ in range(4)]
import time
start = time.time()
for t in threads: t.start()
for t in threads: t.join()
print("耗时:", time.time() - start)
多线程并不能加速,反而因为频繁切换 GIL 更慢!
IO 密集型 示例(效果好)
import threading
import time
def io_task():
time.sleep(2)
threads = [threading.Thread(target=io_task) for _ in range(10)]
start = time.time()
for t in threads: t.start()
for t in threads: t.join()
print("耗时:", time.time() - start)
十个线程几乎同时 sleep,总耗时≈2秒,多线程是有效的。
2. 如何绕过 GIL 限制?
方案一:使用 multiprocessing(多进程)
from multiprocessing import Process
def cpu_task():
count = 0
for _ in range(10**7):
count += 1
procs = [Process(target=cpu_task) for _ in range(4)]
for p in procs: p.start()
for p in procs: p.join()
每个进程有自己的 Python 解释器和内存空间,不受 GIL 限制,适合 CPU 密集型任务。
方案二:使用 C/C++ 写的扩展模块
-
NumPy、Pandas 内部用 C 实现了重计算部分,释放了 GIL,性能更强;
-
可以用
cython或cffi编写扩展模块绕过 GIL。
3. 常见误区
| 错误理解 | 正确认知 |
|---|---|
| GIL 会限制所有并发 | 限制的是 线程的 CPU 并发,非 IO 并发 |
| 多线程一定加速 | CPU 密集型会更慢;只有 IO 密集型适合多线程 |
| Python 不能并发 | 多进程 + IO 并发可以实现高性能并发程序 |
4. 总结
| 内容 | 说明 |
|---|---|
| GIL 作用 | 限制同一时间只能一个线程运行 Python 字节码 |
| 主要影响 | CPU 密集型任务多线程无效、甚至更慢 |
| 避免方法 | 使用多进程、调用 C 扩展、用 asyncio 或其它解释器 |
| 多线程推荐场景 | 网络爬虫、文件读写、数据库请求(IO 密集型) |
九、asyncio(异步编程、协程)
asyncio 是 Python 内置的异步编程库,用于实现高并发的网络或 IO 应用(如爬虫、服务器、异步请求等),核心思想是:
非阻塞 + 协程(Coroutine)调度,不需要多线程也能并发执行。
| 名词 | 含义 |
|---|---|
| 协程(Coroutine) | 可以被挂起和恢复的函数,async def 定义 |
| 事件循环(Event Loop) | 协调调度协程的机制 |
| 任务(Task) | 协程包装后可调度运行的对象 |
| await | 暂停当前协程,等待另一个协程/异步操作完成 |
1. 最简单的 asyncio 示例
import asyncio
async def say_hello():
print("你好")
await asyncio.sleep(1) # 模拟 IO 阻塞
print("你好 again")
asyncio.run(say_hello())
运行结果:
你好
(等待1秒)
你好 again
特点:
-
async def定义协程函数; -
await表示“挂起等待”,不会阻塞整个程序; -
asyncio.run()是启动入口。
2. 多个任务并发执行(核心优势)
import asyncio
async def task(name, delay):
print(f"{name} 开始")
await asyncio.sleep(delay)
print(f"{name} 结束")
async def main():
# 创建任务列表
tasks = [
asyncio.create_task(task("任务A", 2)),
asyncio.create_task(task("任务B", 3)),
asyncio.create_task(task("任务C", 1)),
]
await asyncio.gather(*tasks) # 并发运行
asyncio.run(main())
输出:
任务A 开始
任务B 开始
任务C 开始
(1秒后)任务C 结束
(2秒后)任务A 结束
(3秒后)任务B 结束
三秒内完成了三个任务,而不是串行等待!
3. asyncio 工作原理
原理图简述:
事件循环:
|
|---> 调度协程1(await I/O) → 挂起
|---> 调度协程2(await I/O) → 挂起
|---> 一旦某个 I/O 完成 → 恢复协程执行
它不会新建线程,而是用一个循环不断调度“哪些协程可以继续执行”。
4. 协程 VS 线程对比
| 对比点 | 协程(asyncio) | 线程(threading) |
|---|---|---|
| 创建开销 | 小(几 KB) | 大(几 MB) |
| 切换方式 | 协程调度器控制(用户态) | 操作系统调度器(内核态) |
| 上下文切换代价 | 小 | 大(涉及系统上下文、GIL) |
| 并发能力 | 高(成千上万个协程) | 中等(几十到几百个线程) |
| 编码难度 | 稍高(需要 async/await 结构) | 较简单(同步写法) |
| 适合任务类型 | IO 密集型 | IO 密集型 CPU密集型“不推荐” |
| 是否并行(多核) | 否(单线程) | 否(受 GIL 限制) |
5. 常用 asyncio 组件
| 组件 | 用途 |
|---|---|
asyncio.run() | 启动事件循环并运行协程 |
asyncio.create_task() | 创建任务(立即调度) |
asyncio.gather() | 并发等待多个协程完成 |
asyncio.sleep() | 异步睡眠 |
asyncio.Queue | 异步队列,用于生产者消费者模型 |
asyncio.Semaphore | 限制并发量 |
6. 真实应用场景
异步爬虫
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as resp:
print(f"{url} 状态码: {resp.status}")
async def main():
urls = ["http://example.com"] * 5
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
await asyncio.gather(*tasks)
asyncio.run(main())
使用 aiohttp + asyncio 可发起高效的并发网络请求,远胜多线程。
7. 与同步代码的对比
同步写法:
def foo():
time.sleep(3)
print("结束")
异步写法:
async def foo():
await asyncio.sleep(3)
print("结束")
await 后面必须跟异步函数,不能是普通的阻塞操作!
十、进程池与线程池
1. 进程池 multiprocessing.Pool
当你需要运行大量进程时,不建议手动创建 Process,推荐用进程池统一管理。
示例:
from multiprocessing import Pool
import os, time
def task(x):
print(f"进程 {os.getpid()} 执行任务 {x}")
time.sleep(1)
return x * x
if __name__ == '__main__':
with Pool(processes=4) as pool:
results = pool.map(task, range(10))
print("结果:", results)
输出(部分):
进程 12345 执行任务 0
进程 12346 执行任务 1
...
结果: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
优势:自动并发、自动回收资源、适合批量任务。
2. 线程池 concurrent.futures.ThreadPoolExecutor
适用于大量 IO 密集型任务。
from concurrent.futures import ThreadPoolExecutor
import time
def task(x):
print(f"处理任务 {x}")
time.sleep(1)
return x * 2
with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(task, range(10)))
print("结果:", results)
3. 进程池 concurrent.futures.ProcessPoolExecutor
适用于 CPU 密集型任务,写法类似线程池。
from concurrent.futures import ProcessPoolExecutor
import time
def task(x):
time.sleep(1)
return x * x
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(task, range(10)))
print("结果:", results)
4. 对比
| 项目 | 进程池 | 线程池 |
|---|---|---|
| 模块 | multiprocessing.Pool 或 ProcessPoolExecutor | ThreadPoolExecutor |
| 并发粒度 | 进程,独立解释器 | 线程,共享解释器 |
| 内存 | 高(不共享) | 低(共享) |
| GIL 影响 | 无 | 有 |
| 场景 | CPU 密集(加密、图像处理) | IO 密集(网络、磁盘、爬虫) |
| 任务提交方式 | map、submit | 同 |
| 对比点 | concurrent.futures.ProcessPoolExecutor | multiprocessing.Pool |
|---|---|---|
| 所属模块 | concurrent.futures(Python 3.2+) | multiprocessing(Python 2 就有) |
| 编程风格 | 面向对象,现代异步接口(submit、future) | 函数式风格(apply、map) |
| 返回结果 | Future 对象(可以异步获取) | 立即返回结果或阻塞等待 |
| 错误处理 | 异常封装在 Future 中,易于捕获 | 处理异常较麻烦 |
| 支持异步 | 支持 asyncio 搭配使用(用 loop.run_in_executor) | 不支持协程环境 |
| 可读性 | 更清晰(使用 with 管理资源) | 稍老派,易出错 |
| 适合场景 | 推荐用于现代异步并发、结构清晰的代码 | 更适合简单场景或兼容旧代码 |
| 执行方式 | submit(fn, *args) 非阻塞提交任务 | map, apply, apply_async |
5. submit + as_completed 进阶写法(线程池和进程池通用)
from concurrent.futures import ThreadPoolExecutor, as_completed
def task(x):
time.sleep(1)
return x * 2
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(task, i) for i in range(10)]
for future in as_completed(futures):
print("结果:", future.result())
适合逐个处理结果,而不是 map 那种批量等待。
6. 什么时候用哪种
| 任务类型 | 推荐方式 |
|---|---|
| 网络请求 / 爬虫 | ThreadPoolExecutor |
| 图像处理 / 计算 | ProcessPoolExecutor |
| 批处理任务 | Pool.map() 或 map_async() |
| 高级调度控制 | 手动管理 Process / Queue / Pipe |
7. 总结
| 模块 | 类型 | 场景 | 优点 |
|---|---|---|---|
threading | 线程 | IO 密集 | 开销小、响应快 |
multiprocessing | 进程 | CPU 密集 | 独立解释器、真正并行 |
ThreadPoolExecutor | 线程池 | 网络、多任务 | 自动线程管理,易用 |
ProcessPoolExecutor | 进程池 | 多核计算 | 并行执行,简单易用 |
217

被折叠的 条评论
为什么被折叠?



