1. 并发编程基础:进程、线程与协程
各位同学好,我是你们这门课的老朋友。今天咱们正式开篇,聊聊并发编程最底层的三个概念:进程、线程和协程。说实话,我见过太多人一上来就撸并发代码,结果线上服务各种卡死、数据错乱,最后回过头来才发现——连基础都没打牢。
这一章,咱们就把地基夯实。我会结合自己踩过的坑,把进程、线程、协程的区别,并发与并行的本质,以及Python里那个让人又爱又恨的GIL锁,一次性讲透。
1.1 进程:操作系统的“亲儿子”
进程是什么?说白了,就是程序的一次执行实例。你双击一个浏览器,操作系统就给你创建一个进程。每个进程都有自己独立的地址空间、内存、文件句柄——就像每个孩子都有自己的房间,互不干扰。
我记得刚入行时,有个同事写了个爬虫,开了几十个进程去抓数据。结果服务器直接OOM了。为什么?因为每个进程都有独立的内存空间,开销太大了。进程间切换,需要保存和恢复整个CPU上下文,成本很高。
进程的核心特征:
- 资源隔离:一个进程挂了,不影响其他进程
- 独立地址空间:进程间通信(IPC)需要管道、消息队列、共享内存等机制
- 创建/销毁开销大:系统调用频繁,内存分配多
1.2 线程:轻量级的“打工仔”
线程是进程内部的执行单元。一个进程可以包含多个线程,它们共享进程的内存空间和资源。你想想看,如果每个任务都开一个进程,那服务器早炸了。线程就是来解决这个问题的——它们共享资源,切换成本低得多。
我做过一个高并发网关,最开始用多进程模型,每来一个请求就fork一个子进程。结果并发量一上来,CPU全耗在进程切换上了。后来改成多线程,吞吐量直接翻了三倍。嗯,这里要注意:线程虽然轻量,但共享内存也带来了数据竞争的问题,后面我们会讲锁机制。
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源开销 | 大(独立内存空间) | 小(共享进程资源) |
| 切换成本 | 高(需要切换页表、刷新TLB) | 低(只需保存寄存器) |
| 数据共享 | 复杂(需要IPC机制) | 简单(直接读写共享内存) |
| 安全性 | 高(天然隔离) | 低(一个线程崩溃可能拖垮整个进程) |
1.3 协程:用户态的“微线程”
协程,很多人叫它“微线程”或“纤程”。它比线程更轻量,切换完全在用户态完成,不需要操作系统介入。说白了,就是程序员自己控制执行流的切换。
我曾经优化过一个爬虫服务,用线程池跑,并发200个任务,CPU就飙到80%了。后来改成协程,同样的机器,并发2000个任务,CPU才30%。为什么?因为协程切换就是保存几个寄存器和栈指针,开销几乎可以忽略。
我的建议:
IO密集型任务(网络请求、文件读写、数据库查询)优先考虑协程。CPU密集型任务(计算、加密、压缩)用多进程或线程池。别混用,否则调度起来很头疼。
1.4 并发 vs 并行:别再傻傻分不清
这两个词,面试必问,但很多人理解得模棱两可。我直接给你一个最直白的解释:
- 并发(Concurrency):多个任务在同一个时间段内交替执行。单核CPU也能做到,靠的是时间片轮转。
- 并行(Parallelism):多个任务在同一个时刻同时执行。必须有多核CPU支持。
你想想看,一个厨师(单核CPU)同时做三个菜,他只能切一下菜,炒一下锅,再回去切菜——这是并发。三个厨师(多核CPU)各做一道菜——这才是并行。
我在项目中遇到过很多次,代码里开了几十个线程,以为是在并行处理,结果部署到单核服务器上,性能反而下降了。为什么?因为线程切换本身就有开销,单核上并发太多,反而得不偿失。
避坑指南:
我曾经在一个2核4线程的机器上,给一个计算密集型任务开了8个线程。结果性能比单线程还差。后来用os.cpu_count()获取核心数,把线程数设为核心数+1,性能才正常。记住:并行需要硬件支持,别盲目开线程。
1.5 GIL锁:Python并发的心头痛
聊Python并发,绕不开GIL(Global Interpreter Lock,全局解释器锁)。这东西,说白了就是Python解释器里的一把大锁,保证同一时刻只有一个线程在执行Python字节码。
为什么会这样?因为Python的内存管理不是线程安全的。如果不加GIL,多个线程同时操作引用计数,内存分分钟就崩了。这是CPython实现的一个历史包袱。
我刚开始用Python做并发时,天真地以为多线程能利用多核。结果写了个CPU密集型的计算程序,开了4个线程,跑起来一看,CPU占用率只有100%(单核满载)。换成多进程,CPU才跑到400%。那一刻,我才真正理解了GIL的威力。
| 场景 | 多线程 | 多进程 | 协程 |
|---|---|---|---|
| CPU密集型 | ❌ 受GIL限制,无法利用多核 | ✅ 每个进程有独立GIL,可并行 | ❌ 协程本身不解决并行问题 |
| IO密集型 | ✅ 线程在IO等待时释放GIL | ✅ 但进程开销大,不推荐 | ✅ 最佳选择,开销最小 |
| 混合型 | ⚠️ 部分场景可用 | ✅ 配合进程池使用 | ⚠️ 计算部分需要另开进程 |
GIL的真相:
GIL不是Python语言的限制,而是CPython解释器的实现限制。其他Python解释器(如Jython、IronPython)没有GIL。但CPython是主流,所以咱们得学会和GIL共存。
那怎么绕过GIL?我总结了三条路:
- 用多进程:每个进程有独立的GIL,适合CPU密集型任务。用
multiprocessing模块,注意进程间通信的开销。 - 用协程:单线程内实现高并发,适合IO密集型任务。用
asyncio,配合aiohttp、aiomysql等库。 - 用C扩展:把CPU密集型的部分用C/C++写,通过
ctypes或cffi调用。在C代码里可以释放GIL。
我个人习惯是:IO密集型用协程,CPU密集型用多进程,实在不行再考虑C扩展。别一上来就上C扩展,维护成本太高。
1.6 本章知识体系总览
下面这张图,把本章的核心概念和关系梳理清楚了。建议你保存下来,后面学完整个课程再回来看,会有更深的理解。