2、Python并发编程基础:进程、线程、协程的概念与区别,GIL锁的影响
各位同学,咱们今天聊聊并发编程的三大基石——进程、线程、协程。说实话,很多Python开发者写了几年代码,对这几个概念还是模模糊糊的。我当年刚入行时也一样,总觉得它们差不多,直到在线上环境踩了坑,才真正搞明白它们的区别。
嗯,咱们不搞那些枯燥的理论,我结合自己的实战经验,把这三个东西给你讲透。
2.1 进程:最重的“独立王国”
进程是什么?说白了,就是操作系统里一个正在运行的程序实例。每个进程都有自己的内存空间、文件描述符、环境变量……就像一个独立的王国,互不干扰。
我在项目中遇到过一个问题:一个爬虫服务跑着跑着内存暴涨,最后把整个服务器搞挂了。为什么?因为所有爬虫任务都在同一个进程里,一个任务的内存泄漏,把整个进程拖垮了。
进程的特点很鲜明:
- 资源隔离:进程间内存独立,一个进程挂了不影响其他进程
- 创建开销大:每次创建进程,操作系统都要分配独立的内存空间
- 通信成本高:进程间通信(IPC)需要管道、队列、共享内存等机制
- 适合CPU密集型:可以充分利用多核CPU
核心要点:进程是资源分配的最小单位。每个进程至少包含一个线程。
2.2 线程:轻量级的“车间工人”
线程是进程里的执行单元。一个进程可以包含多个线程,它们共享进程的内存空间。你可以把进程想象成一个工厂,线程就是工厂里的工人,共用厂房和设备。
线程的优势很明显:
- 轻量:创建和切换开销比进程小得多
- 共享内存:线程间可以直接读写全局变量
- 通信方便:不需要复杂的IPC机制
但线程也有麻烦。我记得有一次做高并发Web服务,用多线程处理请求,结果数据经常对不上。排查了半天,发现是多个线程同时修改同一个字典,出现了竞态条件。这就是线程不安全的问题。
避坑指南:我曾经因为没加锁,导致线上订单数据错乱,损失了不少钱。多线程操作共享数据时,一定要加锁或者用线程安全的数据结构。
2.3 协程:用户态的“微线程”
协程,很多人叫它“微线程”或“纤程”。它比线程更轻量,完全由程序员控制调度,而不是操作系统。
你想想看,线程的切换需要操作系统介入,涉及内核态和用户态的切换,开销不小。而协程的切换只在用户态完成,快得多。
协程的核心特点:
- 单线程内并发:一个线程里可以跑成千上万个协程
- 主动让出:协程遇到IO操作时主动让出CPU,等IO完成再回来
- 没有锁的问题:因为协程是串行执行的,不存在竞态条件
Python里实现协程,最常用的就是asyncio库。我做一个高并发API网关时,用协程轻松处理了上万并发连接,而线程方案早就撑不住了。
import asyncio
async def fetch_data(url):
# 模拟IO操作
await asyncio.sleep(1)
return f"Data from {url}"
async def main():
tasks = [fetch_data(f"http://api.example.com/{i}") for i in range(100)]
results = await asyncio.gather(*tasks)
print(f"获取了 {len(results)} 条数据")
asyncio.run(main())
这段代码里,100个协程并发执行,每个遇到await asyncio.sleep(1)时主动让出,总共只用了1秒多就完成了。如果用线程,光创建100个线程的开销就够呛。
2.4 三者的核心区别
咱们用一张表来对比一下:
| 特性 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 资源占用 | 高(独立内存空间) | 中(共享内存) | 极低(栈空间) |
| 创建开销 | 大 | 中 | 极小 |
| 切换开销 | 大(内核态切换) | 中(内核态切换) | 极小(用户态切换) |
| 通信方式 | IPC(队列、管道等) | 共享内存(需加锁) | 直接调用/共享变量 |
| 适用场景 | CPU密集型、高隔离性 | IO密集型、中等并发 | 高并发IO密集型 |
| Python支持 | multiprocessing | threading | asyncio |
2.5 GIL锁:Python的“紧箍咒”
聊Python并发,绕不开GIL(全局解释器锁)。很多初学者一听到GIL就头疼,其实没那么复杂。
GIL是什么?说白了,就是Python解释器里的一把大锁。它保证同一时刻只有一个线程在执行Python字节码。你想想看,这意味什么?多线程在Python里,其实并不能真正并行利用多核CPU。
为什么会这样?Python的内存管理不是线程安全的。如果没有GIL,多个线程同时操作引用计数,内存就乱套了。这是CPython解释器的一个设计取舍。
GIL的影响:
- CPU密集型任务:多线程反而可能比单线程慢,因为线程切换有开销
- IO密集型任务:影响不大,因为线程在等待IO时会释放GIL
- 协程不受影响:协程在单线程内运行,根本不需要GIL
个人经验:我习惯这样选型——CPU密集型用多进程(multiprocessing),IO密集型用协程(asyncio),实在需要线程的场景用threading但注意加锁。别跟GIL硬刚,绕开它才是明智之举。
举个例子,计算斐波那契数列这种纯CPU任务:
import threading
import time
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
# 单线程
start = time.time()
for _ in range(4):
fib(35)
print(f"单线程耗时: {time.time() - start:.2f}s")
# 多线程
start = time.time()
threads = [threading.Thread(target=fib, args=(35,)) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"多线程耗时: {time.time() - start:.2f}s")
你猜结果怎么样?多线程反而更慢!因为GIL让它们无法真正并行,反而多了线程切换的开销。
那怎么绕过GIL?
- 用多进程:每个进程有自己的GIL,可以真正并行
- 用C扩展:像numpy这样的库,底层C代码会释放GIL
- 用协程:单线程内并发,根本不需要GIL
2.6 实战中的选择策略
说了这么多,到底怎么选?我总结了一个简单的决策树:
- 任务主要是CPU计算? → 用多进程(multiprocessing)
- 任务主要是IO等待? → 用协程(asyncio)
- 需要大量共享状态? → 用线程(threading)+ 锁
- 既要CPU又要IO? → 多进程 + 协程组合使用
我在做一个实时数据处理系统时,就用了多进程+协程的组合。主进程负责接收数据,每个子进程里跑协程处理数据,既利用了多核,又实现了高并发。效果非常好。
总结一下:进程、线程、协程各有各的用武之地。GIL是Python的固有特性,别抱怨它,学会跟它共处。记住一句话——CPU密集型用多进程,IO密集型用协程,线程用在中间地带。
下一章咱们会深入讲多进程编程,包括进程池、进程间通信、共享内存等实战技巧。到时候我会分享更多踩坑经验,敬请期待。