Python 并发编程入门:线程、asyncio 与适用场景
为什么需要并发
写爬虫、批量请求接口、读写文件时,程序常常不是在计算,而是在“等”。等网络响应、等磁盘读写。单线程顺序执行时,这些等待时间都被白白浪费。并发就是让程序在等待的时候去做别的事,从而缩短总耗时。Python 里最常用的两种并发方式是 threading(线程)和 asyncio(异步),它们各有适用场景。
线程:适合 I/O 密集任务
threading 让多个线程在同一个进程里交替运行。遇到网络请求这类 I/O 操作时,线程会主动让出执行权,另一个线程继续干活。下面是一个用线程并发下载的示意:
import threading
import requests
urls = ["https://example.com/a", "https://example.com/b", "https://example.com/c"]
def fetch(url):
r = requests.get(url, timeout=10)
print(url, r.status_code)
threads = [threading.Thread(target=fetch, args=(u,)) for u in urls]
for t in threads:
t.start()
for t in threads:
t.join()
线程的优点是写法直观,和同步代码几乎一样;缺点是线程切换有开销,线程数上千后就不划算,而且多个线程共享内存时要小心竞态条件,必要时用 Lock 保护共享数据。
asyncio:单线程里的协作式并发
asyncio 用一个事件循环驱动多个协程。协程在 await 处主动挂起,事件循环趁机运行别的协程,全程只有一个线程,没有线程切换开销,也没有数据竞争。适合高并发 I/O 场景:
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url, timeout=10) as resp:
return url, resp.status
async def main():
urls = ["https://example.com/a", "https://example.com/b", "https://example.com/c"]
async with aiohttp.ClientSession() as session:
results = await asyncio.gather(*(fetch(session, u) for u in urls))
print(results)
asyncio.run(main())
怎么选
可以按下面这张表做初步判断:
- 任务量不大、代码简单:threading,上手成本最低。
- 成百上千个并发 I/O:asyncio,资源占用更小。
- CPU 密集计算:两者都不合适,考虑 multiprocessing,或者把计算部分交给扩展库。
- 已有大量同步代码要改造:先用线程池(concurrent.futures)过渡,再逐步迁移。
几个常见坑
第一,GIL 的存在意味着同一时刻只有一个线程在执行 Python 字节码,线程并不能让纯计算加速。第二,不要在 asyncio 协程里调用 time.sleep、requests 这类阻塞函数,它们会卡住整个事件循环,应该用 asyncio.sleep 和支持异步的库。第三,并发数不是越多越好,对方服务器和本地带宽都有上限,配合信号量(asyncio.Semaphore)做限流更稳妥。
小结
并发不是银弹,先分清任务是 I/O 密集还是 CPU 密集,再选工具。I/O 密集的小规模任务用 threading,大规模 I/O 用 asyncio,计算密集用多进程。理解了这层取舍,写并发代码时就很少会选错方向。
- 点赞
- 收藏
- 关注作者
评论(0)