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
什么叫同步:
这是一个简单的同步任务
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()
区别
-
事件循环的生命周期管理:
asyncio.run()
自动管理事件循环的生命周期,而手动获取事件循环的方式需要你自己管理。 -
简洁性:
asyncio.run(main())
更简洁,因为它隐藏了事件循环的创建和关闭。 -
已存在事件循环的兼容性:如果你已经有一个正在运行的事件循环(例如在GUI应用程序或某些框架中),那么你应该使用
loop.run_until_complete(main())
,因为asyncio.run()
会创建一个新的事件循环,这可能会导致冲突。 -
异常处理:
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
库的一部分,用于明确地创建新的任务。
区别
-
语义明确性:
asyncio.create_task()
的语义更加明确,它专门用于创建任务。而asyncio.ensure_future()
则更加通用,它可能返回一个已经存在的任务,或者创建一个新任务。 -
用法差异:在大多数情况下,你可以使用
asyncio.create_task()
来替代asyncio.ensure_future()
,因为现代版本的asyncio
库推荐使用create_task
来创建新任务。然而,如果你正在处理一个可能是任务或协程的对象,并且你想要确保它被调度,那么ensure_future
可能会更合适。 -
兼容性:
asyncio.ensure_future()
在较早的Python版本中就已经存在,因此如果你的代码需要兼容旧版本的Python,你可能需要使用ensure_future
。