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

asyncio模块

协程对象(coroutine object),缩写coro,俗称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)

async def main():  
    tasks = []  
    for i in range(1, 4): 
        coro = run(i)    # 创建协程对象coro
        task = asyncio.ensure_future(coro)  # 显式地创建任务,可以接受(任务或协程的对象)
        # task = asyncio.create_task(coro)  # 显式地创建任务,只能接受(协程对象)
        tasks.append(task)  # 将task任务对象添加到tasks任务列表中  
    await asyncio.gather(*tasks)  # 使用gather等待所有任务完成
    
if __name__ == '__main__':
    # 创建一个新的事件循环 loop
    loop = asyncio.get_event_loop()
    # 把我们的多任务注册到事件循环上,并等待所有任务完成
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()
    # try 块包含了调用 loop.run_until_complete(main()) 的代码,它会运行 main 函数直到它完成。finally 块则确保无论 main 函数是否抛出异常,事件循环都会被关闭。

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

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

 

 

以下代码:

# 创建一个新的事件循环 loop
loop = asyncio.get_event_loop()
# 把我们的多任务注册到事件循环上,并等待所有任务完成
try:
    loop.run_until_complete(main())
finally:
    loop.close()
# try 块包含了调用 loop.run_until_complete(main()) 的代码,它会运行 main 函数直到它完成。finally 块则确保无论 main 函数是否抛出异常,事件循环都会被关闭。

可以等效替换为一句话代码:

从 Python 3.7 开始,推荐使用 asyncio.run(main()) 来运行异步主函数,因为它会自动创建loop事件和关闭事件循环,使代码更加简洁:

asyncio.run(main())

在Python的asyncio库中,asyncio.run(main())asyncio.get_event_loop().run_until_complete(main())都是用来运行异步主函数的方式,但它们之间存在一些重要的区别。

asyncio.run(main())

asyncio.run(main())是Python 3.7及更高版本中引入的一个便捷函数,用于执行顶层的异步代码。它创建一个新的事件循环,运行传入的异步函数,并等待其完成,然后关闭事件循环。asyncio.run()负责处理事件循环的生命周期,包括创建、运行和关闭,使得编写异步程序的入口点更加简洁。

优点:

  • 简洁:不需要手动创建和关闭事件循环。
  • 自动处理事件循环的生命周期。

缺点:

  • 不适合在已经存在事件循环的环境中使用,因为它会创建一个新的事件循环。

loop = asyncio.get_event_loop().run_until_complete(main())

这种方式更加底层,需要手动获取事件循环并调用其run_until_complete()方法来运行异步函数。在Python 3.4及更高版本中,你可以使用asyncio.get_event_loop()来获取当前的事件循环,或者在需要时创建一个新的事件循环。

优点:

  • 更加灵活:允许你在已经存在的事件循环中运行代码。
  • 控制力更强:你可以更好地控制事件循环的生命周期和其他方面。

缺点:

  • 需要更多的代码:需要手动获取事件循环并调用run_until_complete()
  • 需要更多的管理:需要确保事件循环在合适的时候被关闭,避免资源泄露。loop.close()

区别

  1. 事件循环的生命周期管理asyncio.run()自动管理事件循环的生命周期,而手动获取事件循环的方式需要你自己管理。

  2. 简洁性asyncio.run(main())更简洁,因为它隐藏了事件循环的创建和关闭。

  3. 已存在事件循环的兼容性:如果你已经有一个正在运行的事件循环(例如在GUI应用程序或某些框架中),那么你应该使用loop.run_until_complete(main()),因为asyncio.run()会创建一个新的事件循环,这可能会导致冲突。

  4. 异常处理asyncio.run()会捕获并重新抛出main()函数中抛出的任何异常,而手动管理事件循环的方式需要你自己处理这些异常。

总结

如果你正在编写一个独立的异步脚本或应用程序,并且想要一个简洁的入口点,那么asyncio.run(main())是一个很好的选择。然而,如果你需要在已存在的事件循环中运行代码,或者需要更多的控制力,那么手动获取事件循环并使用run_until_complete()可能更合适。

 

区别对比2:

task = asyncio.ensure_future(coro)  # 显式地创建任务,可以接受(任务或协程的对象)

task = asyncio.create_task(coro)  # 显式地创建任务,只能接受(协程对象)

asyncio.ensure_future()是一个较为通用的函数,它可以接受一个协程或可等待对象,并返回一个任务对象。如果传入的对象已经是一个任务,它会直接返回该对象;如果传入的是一个协程,它会创建一个新任务并返回。这个函数在早期的asyncio版本中就已经存在,用于确保对象可以被调度和执行。 

asyncio.create_task()是一个较新的函数,专门用于创建任务。它接受一个协程对象,并返回一个任务对象。这个函数是在Python 3.7中引入的,作为asyncio库的一部分,用于明确地创建新的任务。

区别

  1. 语义明确性asyncio.create_task()的语义更加明确,它专门用于创建任务。而asyncio.ensure_future()则更加通用,它可能返回一个已经存在的任务,或者创建一个新任务。

  2. 用法差异:在大多数情况下,你可以使用asyncio.create_task()来替代asyncio.ensure_future(),因为现代版本的asyncio库推荐使用create_task来创建新任务。然而,如果你正在处理一个可能是任务或协程的对象,并且你想要确保它被调度,那么ensure_future可能会更合适。

  3. 兼容性asyncio.ensure_future()在较早的Python版本中就已经存在,因此如果你的代码需要兼容旧版本的Python,你可能需要使用ensure_future

posted @ 2024-03-05 11:57  Magiclala  阅读(8)  评论(0编辑  收藏  举报