Skip to content

和 Python 相关的异步

可以参考的一些资料

Python 异步编程入门 - 阮一峰的网络日志

【Py】asyncio:为异步编程而生 | Python 特性 | 并发编程 | 协程_哔哩哔哩_bilibili

【python】await机制详解。再来个硬核内容,把并行和依赖背后的原理全给你讲明白_哔哩哔哩_bilibili

Python asyncio 的基本模型

asyncio 模型其实只存在一个线程,本质和 JS 是一样的,使用协作式多任务(cooperative multitasking),所有的任务通过 await 主动让出控制权,在编程的时候,对于一些阻塞主线程的操作就可以使用 await

控制权的转换

Python asyncio 的核心就是控制权的转换,当一个协程 await 某个异步操作时,当前协程暂停执行,控制权交还给“事件循环”,事件循环再调度其他“准备好执行”的协程

角色说明
事件循环(Event Loop)控制所有协程的调度,管理任务队列
协程对象(Coroutine Object)async def 函数调用的结果,是可以“暂停和恢复”的对象
Task协程的包装器,让事件循环能追踪其状态(Pending、Running、Done)
Future表示一个“将来才有结果”的对象,可能是网络、I/O 等

具体的实现:

python
import asyncio

async def download():
    print('开始下载!')
    await asyncio.sleep(1)
    print('下载完成')
    return "Ciallo"

async def main():
    results = await asyncio.gather(download(), download())
    print(results)

asyncio.run(main())
  1. asyncio.run(main()) 启动顶层协程
  2. main() 中,使用 asyncio.gather(),将 download() 封装成 Task,使得事件循环能追踪到其状态,由事件循环调度
  3. 开始执行 download() 里面的内容的时候,先执行 print('开始下载!')
  4. 执行到 await asyncio.sleep(1) 的时候
    • Sleep (1) 返回一个 Future(表示“1 秒后完成”);
    • 当前协程挂起,将自己注册为这个 Future 的“回调”;
    • 控制权交还给事件循环(event loop);
  5. 事件循环去执行别的协程
  6. 1 秒后,Future 标记为 Done
  7. 事件循环发现 Future 已经完成了,恢复之前挂起的协程,继续执行下面的 print('下载完成')
[foo() Task]   ---> await sleep(1) --挂起-->    [Event Loop 控制权]
       ^                                             |
       |<--- sleep 完成 ---- Future.set_result() <---|

Task 的作用和封装

  • Task 是对协程的封装
  • 它的作用是:让事件循环能够调度这个协程,追踪它的执行状态,处理它的完成/异常等
python
import asyncio

async def say_hello():
    await asyncio.sleep(1)
    print("Hello")

async def main():
    task = asyncio.create_task(say_hello())  # <--- 封装成 Task
    await task  # 等待 task 执行完成

asyncio.run(main())
  • 其中的 say_hello() 就是一个协程对象(coroutine object)
  • asyncio.create_task() 就会把协程对象封装为 Task
  • 然后,事件循环会自动开始执行这个 Task(即把协程注册进去调度)
  • await task 是等待 Task 的执行过程,并不是封装的过程
async def say_hello()[协程对象] --> asyncio.create_task()[封装为Task] --> 送入到 Event Loop

本质

无论 await 的是 sleep、socket I/O,还是磁盘 I/O,本质都是:挂起当前协程,把“完成后要做的事”注册进事件循环中,一旦完成,重新唤醒协程继续执行

这就是异步的核心机制,用 Future + 回调唤醒实现“非阻塞的等待”

或者理解为一个大摩天轮,Event Loop 就是摩天轮的骨架,一直在转,而协程就是摩天轮客舱里面的东西,Task 就是客舱外面贴的状态(标志)

实践

网页的异步抓取

python
import aiohttp
import asyncio

# 单个网页的异步抓取
async def fetch(session, url):
    async with session.get(url) as response:
        print("status:", response.status)

async def main():

    urls = ['https://www.juniortree.com', 'https://note.juniortree.com']

    # 复用一个连接池
    async with aiohttp.ClientSession() as session:
        # 任务的一个列表
        tasks = [fetch(session, url) for url in urls]
        # *tasks 参数解包,等同于 fetch(session, 'https://www.juniortree.com') 和 fetch(session, 'https://note.juniortree.com')
        await asyncio.gather(*tasks)
       
asyncio.run(main())

为什么要使用 async with,它可以帮助我们自动管理资源,也可以用 catch/finally 来处理:

python
# 单个网页的异步抓取
async def fetch(session, url):
    response  = await session.get(url)
    try:
        print(response.status)
    finally:
        response.close()
python
# 复用一个连接池
    session = aiohttp.ClientSession()
    try: 
        tasks = tasks = [fetch(session, url) for url in urls]
        # *tasks 参数解包,等同于 fetch(session, 'https://www.juniortree.com') 和 fetch(session, 'https://note.juniortree.com')
        await asyncio.gather(*tasks)
    finally:
        await session.close()

这种方法需要自己记得手动关闭连接,最好还是使用 async with

最后更新于:

Released under the MIT License.