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 倍。

概念对了,代码怎么写都不会跑偏。