Python web crawler(9)多任务同步、异步(协程)

Magiclala的博客 / 2024-03-05 / 原文

asyncio模块

协程对象(coroutine object),缩写coro

概述

  • asyncio模块

    是python3.4版本引入的标准库,直接内置了对异步IO的操作

  • 编程模式

    是一个消息循环,我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO

  • 说明

    到目前为止实现协程的不仅仅只有asyncio,tornado和gevent都实现了类似功能

  • 关键字的说明

    关键字 说明
    event_loop 消息循环,程序开启一个无限循环,把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数
    coroutine 协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用
    task 任务,一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含了任务的各种状态
    async/await python3.5用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口

什么叫同步:

这是一个简单的同步任务

import time

def run(c):
    print('任务开始=====', c)
    time.sleep(4)
    # time.sleep(random.randint(2, 9))
    print('任务完成=====', c)


if __name__ == '__main__':
    t1 = time.time()
    for i in range(1, 4):
        run(i)
    t2 = time.time()
    print("总耗时:%.2f" % (t2 - t1))

运行过程

任务开始===== 1
任务完成===== 1
任务开始===== 2
任务完成===== 2
任务开始===== 3
任务完成===== 3
总耗时:12.00

什么叫协程异步:

把同步任务改造成异步任务(协程)

import asyncio
import random
import time


async def run(i):
    print('任务开始=====', i)
    # await asyncio.sleep(random.randint(2, 9))
    await asyncio.sleep(4)
    print('任务完成=====', i)


if __name__ == '__main__':
    t1 = time.time()
    task_list = []
    for i in range(1, 4):
        c = run(i)  # 协程对象
        task = asyncio.ensure_future(c)
        task_list.append(task)

    # 创建一个新的事件循环 loop
    loop = asyncio.get_event_loop()
    # 使用loop.run_until_complete把我们多任务的列表注册到事件循环上,因为task_list是一个列表,需要被asyncio.wait()处理一次
    loop.run_until_complete(asyncio.wait(task_list))
    t2 = time.time()
    print("总耗时:%.2f" % (t2 - t1))

运行过程

任务开始===== 1
任务开始===== 2
任务开始===== 3
任务完成===== 1
任务完成===== 2
任务完成===== 3
总耗时:4.02

改造第1步,导入函数

import asyncio

改造第2步,把“普通函数”改造成“协程函数”

def run(i):  --> async def run(i):

改造第3步 ,time.sleep()是同步代码写法,协程阻塞写法应该使用asyncio.sleep()

time.sleep()  --> asyncio.sleep()

改造第4步 ,使用await挂起阻塞调用

asyncio.sleep()  --> await asyncio.sleep()

async定义的函数def run(i) ,里面的耗时任务asyncio.,必须被await挂起,

改造第5步 ,将主题函数中的运行函数,改造成“协程对象”

run(i):  --> c = run(i)

改造第6步 ,创建task任务,并把run塞入

task = asyncio.ensure_future(c)

改造第7步 ,把task任务统一放入事件循环中,因此提前创建一个task_list = []空列表,再把每次for循环出来的分task任务依次接收进来

task_list = []

task_list.append(task)

改造第8步 ,创建事件循环、把多任务列表加入事件循环种

# 创建一个新的事件循环 loop
loop = asyncio.get_event_loop()
# 使用loop.run_until_complete把我们多任务的列表注册到事件循环上,因为task_list是一个列表,需要被asyncio.wait()处理一次
loop.run_until_complete(asyncio.wait(task_list))

使用 asyncio.get_event_loop() 和 loop.run_until_complete(asyncio.wait(task_list))方法:

这种方式是较早期的方式,它直接获取事件循环并运行直到一组任务完成。asyncio.wait(task_list) 会返回一个 future 对象,当所有的任务都完成或者某个任务抛出异常时,这个 future 对象就会完成。run_until_complete 会阻塞当前线程,直到这个 future 对象完成。

asyncio.get_event_loop()这种方式的缺点是它更底层,需要显式地获取和关闭事件循环。而且,在 Python 3.7 及更高版本中,不应该在已经存在运行中的事件循环的情况下被调用,否则它会抛出一个 RuntimeError

因此需要在loop.run_until_complete()中加入asyncio.wait(task_list)),因为task_list不是一个coroutine任务,而是多个coroutine任务组成的列表

 

使用 await asyncio.gather(*tasks)代替

await asyncio.gather(*tasks) 是更现代和推荐的方式,它简化了协程的执行流程。asyncio.gather 会接收一组协程,并返回一个 future,这个 future 会在所有给定的协程都完成时完成。使用 await 关键字可以直接等待这个 future 完成,无需显式地获取和关闭事件循环。

import asyncio
import random

async def run(c):
    print('开启任务=====', c)
    await asyncio.sleep(4)
    print('结束任务=====', c)

if __name__ == '__main__':
    tasks = []
    for i in range(1, 4):
        task = run(i)  # 协程对象
        tasks.append(task)  # 直接组成对象列表
    await asyncio.gather(*tasks)

用了异步网络请求,那么你应该多选择使用 await asyncio.gather(*tasks)。这种方式更加简洁,并且是现代 Python 异步编程的推荐做法。

使用 asyncio.gather(*tasks) 的好处在于它可以同时运行多个任务(即多个协程),并且等待它们全部完成。这种方式在处理 I/O 密集型任务(如网络请求或文件读写)时特别有效,因为它可以在单个线程上实现并发执行,避免了多线程或多进程带来的额外开销。