多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE

技术文章

聚焦一线编程技术原理与实战踩坑,沉淀可复用的工程经验。

Python asyncio 异步编程从入门到落地

Python asyncio 异步编程从入门到落地

asyncio 是 Python 3.4 引入、3.5+ 转正的异步 I/O 框架,凭借单线程协程模型在 I/O 密集场景下展现出远超多线程的并发能力。本文从协程本质讲起,覆盖事件循环、任务调度,并以一个高并发异步爬虫收尾,帮你把 asyncio 真正落地到工程中。

一、协程:可暂停的函数

协程(Coroutine)是一种用户态的轻量级并发单元。与普通函数"一调到底"不同,协程可以在执行中途挂起(await),交出控制权,等条件满足后再恢复执行。它不依赖操作系统线程切换,开销极小。

# 用 async def 定义协程函数
async def fetch_data(url):
    print(f"开始请求 {url}")
    await asyncio.sleep(1)   # 模拟 I/O 等待,挂起协程
    print(f"完成 {url}")
    return f"data:{url}"

# 直接调用协程函数只会得到协程对象,不会执行
coro = fetch_data("https://mzph.cn")
# 必须交给事件循环调度才会真正运行

关键字 async def 定义的是协程函数,调用它返回一个协程对象(coroutine object),对象本身不会执行,必须由事件循环驱动。

二、事件循环:asyncio 的心脏

事件循环(Event Loop)负责调度协程:它不断从就绪队列取出可执行的协程运行,遇到 await 挂起的就放回等待队列,等 I/O 完成再唤醒。所有协程都在同一个线程内被循环调度。

import asyncio

async def main():
    # 并发执行多个协程
    results = await asyncio.gather(
        fetch_data("url-1"),
        fetch_data("url-2"),
        fetch_data("url-3"),
    )
    print(results)

# Python 3.7+ 推荐入口
asyncio.run(main())

asyncio.run() 是推荐的顶层入口,它会自动创建事件循环、运行主协程、并在结束后清理。注意 asyncio.run 在一个进程内应只调用一次。

核心心智模型:asyncio 是"协作式"并发,协程主动 await 才会切换;不要在协程里写同步阻塞代码(如 time.sleep、requests.get),否则会卡死整个循环。

三、Task 与并发调度

把协程包装成 Task 后,事件循环会立即调度它运行,而不必等 await。这是实现并发的关键。

async def worker(name, seconds):
    for i in range(3):
        await asyncio.sleep(seconds)
        print(f"{name}: 第 {i+1} 次")

async def main():
    # 方式一:create_task 显式创建任务,立即开始
    t1 = asyncio.create_task(worker("A", 0.5))
    t2 = asyncio.create_task(worker("B", 0.7))
    await t1
    await t2

    # 方式二:gather 一次性并发收集结果
    await asyncio.gather(
        worker("C", 0.3),
        worker("D", 0.4),
    )

asyncio.run(main())

gather 会按传入顺序返回结果列表,即使后启动的任务先完成,结果顺序也保持一致。如果某个任务抛异常,默认会取消其他任务。

四、异步 I/O 与 await 的本质

await 的作用是:把当前协程挂起,把控制权交回事件循环,等到 await 的对象(Future/Task/协程)完成后恢复。挂起期间线程可以去执行其他就绪协程,从而实现并发。

# 真实异步 HTTP 请求(需 aiohttp)
import aiohttp

async def fetch(session, url):
    async with session.get(url) as resp:
        # await 让出控制权,等网络 I/O 完成
        return await resp.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, "https://mzph.cn")
        print(len(html))

asyncio.run(main())

五、实战:高并发异步爬虫

下面用 asyncio + aiohttp 实现一个限制并发数的批量爬虫,演示信号量控制、超时处理与错误重试。

import asyncio, aiohttp, time

class AsyncSpider:
    def __init__(self, urls, concurrency=10, timeout=10):
        self.urls = urls
        self.sem = asyncio.Semaphore(concurrency)  # 并发上限
        self.timeout = aiohttp.ClientTimeout(total=timeout)
        self.results = {}

    async def fetch(self, session, url, retry=2):
        async with self.sem:  # 获取信号量,超出则等待
            for attempt in range(retry + 1):
                try:
                    async with session.get(url) as resp:
                        text = await resp.text()
                        self.results[url] = len(text)
                        print(f"OK {url} -> {len(text)} bytes")
                        return
                except Exception as e:
                    if attempt == retry:
                        print(f"FAIL {url}: {e}")
                    else:
                        await asyncio.sleep(0.5)

    async def run(self):
        start = time.time()
        async with aiohttp.ClientSession(timeout=self.timeout) as session:
            tasks = [self.fetch(session, u) for u in self.urls]
            await asyncio.gather(*tasks)
        print(f"共 {len(self.urls)} 个,耗时 {time.time()-start:.2f}s")

if __name__ == "__main__":
    urls = [f"https://httpbin.org/delay/{i%3}" for i in range(30)]
    asyncio.run(AsyncSpider(urls, concurrency=10).run())

这段代码的核心在于 asyncio.Semaphore:它把并发数限制在 10,避免瞬时打开过多连接打垮目标服务器或本地资源。30 个请求理论耗时约等于 30/10 × 2s ≈ 6s,而同步爬虫需要 30 × 2 = 60s,提升近 10 倍。

六、常见踩坑提示

  • 在协程里调用阻塞函数:如 time.sleeprequests.get 会卡住整个循环。改用 asyncio.sleepaiohttp,或用 run_in_executor 丢给线程池。
  • 忘记 await:协程不 await 只是创建了对象,不会执行,运行时会得到 "coroutine was never awaited" 警告。
  • 在 async 函数里用 asyncio.runrun 只能在同步代码入口调用,协程内应直接 await。
  • 混用同步库:传统同步库(如 requests、pymysql)会破坏异步模型的并发优势,应选用 async 版本。

asyncio 的精髓在于"用同步的代码风格写异步逻辑"。掌握协程与事件循环的协作模型后,你会发现它在爬虫、API 网关、WebSocket 服务等 I/O 密集场景下几乎是性价比最高的方案。

返回列表