Python web crawler(9)多任务同步、异步(协程)
asyncio模块
协程对象(coroutine object),缩写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)
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 密集型任务(如网络请求或文件读写)时特别有效,因为它可以在单个线程上实现并发执行,避免了多线程或多进程带来的额外开销。