1. 异步编程基础:理解阻塞与非阻塞、同步与异步、并发与并行
说实话,我见过太多 Python 开发者,一上来就猛学 asyncio、await 这些语法。
结果呢?写出来的代码要么跑不起来,要么性能还不如同步版本。
问题出在哪?说白了,就是没搞懂最基础的概念。
今天咱们就把这几个容易混淆的概念,掰开揉碎了讲清楚。
1.1 阻塞 vs 非阻塞:你在等,还是不等?
先问个问题:你调用一个函数,它要花 3 秒才能返回结果。
这 3 秒里,你的程序在干嘛?
阻塞:程序就傻傻地等在那里,啥也不干。
非阻塞:程序说「我先去干别的,你好了叫我」。
我举个例子。你去咖啡店点杯咖啡。
- 阻塞模式:你站在柜台前,死死盯着咖啡师做咖啡。这 3 分钟你啥也干不了。
- 非阻塞模式:你拿了号,回座位刷手机。咖啡好了,服务员喊你「XX 号,您的咖啡好了」。
在代码里,阻塞调用最常见的就是 time.sleep() 和 requests.get()。
import time
def blocking_task():
print("开始下载...")
time.sleep(3) # 阻塞 3 秒,CPU 在空转
print("下载完成!")
print("做点别的事")
blocking_task() # 这 3 秒,程序卡在这里
print("这行要等 3 秒后才能执行")
核心区别:阻塞调用会挂起当前线程,非阻塞调用会立即返回。
我个人习惯把阻塞理解为「死等」,非阻塞理解为「留个电话,回头打给你」。
1.2 同步 vs 异步:谁来通知你?
这两个概念经常和阻塞/非阻塞混在一起。我简单区分一下:
- 同步:你主动去问「好了没?好了没?好了没?」
- 异步:对方主动告诉你「好了,结果给你」
还是咖啡店的例子:
- 同步:你每隔 10 秒跑去柜台问「我的咖啡好了吗?」——这叫轮询。
- 异步:你坐那玩手机,咖啡师做好后喊你——这叫回调或通知。
我在项目中遇到过一个问题:用同步方式去查数据库,每次查询都要等 200ms。
如果查 100 次,就是 20 秒。用户早就关页面了。
后来改成异步,发请求后不等待,继续处理其他事。等所有查询都返回了,再统一处理结果。
import asyncio
async def fetch_data(url):
print(f"开始请求 {url}")
await asyncio.sleep(2) # 模拟网络请求
print(f"请求完成 {url}")
return f"数据来自 {url}"
async def main():
# 同步方式:一个一个等
# result1 = await fetch_data("url1")
# result2 = await fetch_data("url2")
# 异步方式:同时发起,谁先完成谁先返回
tasks = [fetch_data("url1"), fetch_data("url2")]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
注意:同步不一定阻塞,异步不一定非阻塞。但 Python 里,我们通常把「同步阻塞」和「异步非阻塞」放在一起用。
1.3 并发 vs 并行:看起来快,还是真的快?
这两个概念,我当年也搞混了很久。
并发(Concurrency):看起来是同时在做多件事,其实是快速切换。
并行(Parallelism):真正的同时做多件事,需要多核 CPU。
再举个例子:
- 并发:你一个人同时看三本书。看 5 分钟 A,换 B 看 5 分钟,再换 C。宏观上你「同时」在看三本,但微观上你一次只看一本。
- 并行:你、你老婆、你儿子三个人,每人看一本书。这才是真正的「同时」。
Python 的 asyncio 是并发,不是并行。它只有一个线程,靠事件循环来回切换任务。
多线程和多进程才是并行(多核情况下)。
| 特性 | 并发 | 并行 |
|---|---|---|
| 核心数 | 单核即可 | 需要多核 |
| 执行方式 | 任务切换 | 同时执行 |
| Python 实现 | asyncio、协程 | 多进程、多线程 |
| 适用场景 | I/O 密集型 | CPU 密集型 |
一句话总结:并发是「看起来快」,并行是「真的快」。
1.4 为什么 Python 的异步特别重要?
你想想看,Python 有个 GIL(全局解释器锁)。
这意味着,多线程在 CPU 密集型任务上,其实没法真正并行。
但 I/O 密集型任务不一样。比如网络请求、文件读写、数据库查询——这些操作大部分时间都在等待。
等待的时候,CPU 是空闲的。
异步编程就是利用这个「等待时间」,去干别的事。
我曾经优化过一个爬虫项目。同步版本爬 1000 个页面,要 15 分钟。
改成 asyncio + aiohttp 后,同样的任务,2 分钟搞定。
性能提升了 7 倍多。为什么?因为网络请求的等待时间被充分利用了。
记住这个公式:异步 ≠ 快,异步 ≠ 省 CPU。异步 = 更高效地利用等待时间。
1.5 避坑指南:我踩过的那些坑
我曾经犯过一个低级错误:在异步函数里调用了同步的 time.sleep()。
结果整个事件循环都被阻塞了,其他协程全卡住。
正确的做法是用 asyncio.sleep()。
还有一个坑:很多人以为用了 async/await 就自动变快了。
不是的。如果你的代码里没有 I/O 等待,全是 CPU 计算,那异步反而更慢——因为多了协程切换的开销。
# 错误示范:异步做 CPU 密集型任务
async def bad_example():
result = 0
for i in range(10_000_000):
result += i # CPU 计算,没有等待
return result
# 正确做法:用多进程处理 CPU 密集型
from multiprocessing import Pool
def cpu_intensive():
result = 0
for i in range(10_000_000):
result += i
return result
with Pool() as pool:
results = pool.map(cpu_intensive, range(4))
重要提醒:异步不是银弹。I/O 密集型用异步,CPU 密集型用多进程。选错了,性能反而下降。
1.6 小结:一张图记住这些概念
我习惯用这张表来帮助记忆:
| 概念 | 核心问题 | 类比 |
|---|---|---|
| 阻塞 vs 非阻塞 | 调用后是否立即返回 | 死等 vs 留电话 |
| 同步 vs 异步 | 谁主动通知结果 | 轮询 vs 回调 |
| 并发 vs 并行 | 是否真正同时执行 | 一人看三本 vs 三人各看一本 |
嗯,基础概念就讲到这里。
下一章,我们会深入 Python 的 asyncio 库,看看事件循环到底是怎么工作的。
到时候,我会用实际代码演示,这些概念在 Python 里是怎么落地的。
记住:搞懂这些概念,比学会 async/await 语法重要 10 倍。
概念对了,代码怎么写都不会跑偏。