先来看一段简单的代码:
import time
def crawl_page(url):
print('crawling {}'.format(url))
sleep_time = int(url.split('_')[-1])
time.sleep(sleep_time)
print('OK {}'.format(url))
def main(urls):
for url in urls:
crawl_page(url)
if __name__ == '__main__':
start_time = time.perf_counter()
main(['url_1','url_2','url_3','url_4'])
end_time = time.perf_counter()
print('run {} seconds'.format(end_time - start_time))
上述代码是爬虫的简单示例,四个url一共用了10s的时间,如何用协程进行优化呢?
import time
import asyncio
async def crawl_page(url):
print('crawling {}'.format(url))
sleep_time = int(url.split('_')[-1])
time.sleep(sleep_time)
print('OK {}'.format(url))
async def main(urls):
for url in urls:
await crawl_page(url)
if __name__ == '__main__':
start_time = time.perf_counter()
asyncio.run(main(['url_1','url_2','url_3','url_4']))
end_time = time.perf_counter()
print('run {} seconds'.format(end_time - start_time))
1、当遇到 await 关键字时,协程会暂停自己的执行。
2、执行 await 后面的异步函数调用,将控制权交给异步函数。
3、异步函数开始执行异步操作,可能会暂时挂起自己的执行,等待异步操作完成。
4、在异步操作完成后,异步函数会恢复执行,并返回结果。
5、协程接收到异步函数的返回结果,恢复执行 await 语句后面的代码。
总结一下就是两个作用:
a、暂停当前协程的执行,将控制权交给事件循环(Event Loop),允许其他任务继续执行。在等待的过程中,协程不会阻塞整个程序或线程,而是允许其他协程或任务继续执行
b、等待异步操作的完成,并获取其返回的结果
运行上述代码发现还需要10s,原因是await关键字是同步调用,crawl_page(url)在当前的调用结束之前,是不会触发下一次调用的,相当于用异步接口写了同步代码
import time
import asyncio
async def crawl_page(url):
print('crawling {}'.format(url))
sleep_time = int(url.split('_')[-1])
await asyncio.sleep(sleep_time)
print('OK {}'.format(url))
async def main(urls):
tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
for task in tasks:
await task
if __name__ == '__main__':
start_time = time.perf_counter()
asyncio.run(main(['url_1','url_2','url_3','url_4']))
end_time = time.perf_counter()
print('run {} seconds'.format(end_time - start_time))
上述代码只需要4s左右,运行总时长等于运行时间最长的爬虫
对于task,还有另一种写法?
import time
import asyncio
async def crawl_page(url):
print('crawling {}'.format(url))
sleep_time = int(url.split('_')[-1])
await asyncio.sleep(sleep_time)
print('OK {}'.format(url))
async def main(urls):
tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
await asyncio.gather(*tasks)
if __name__ == '__main__':
start_time = time.perf_counter()
asyncio.run(main(['url_1','url_2','url_3','url_4']))
end_time = time.perf_counter()
print('run {} seconds'.format(end_time - start_time))
其它用法:
import asyncio
async def worker_1():
await asyncio.sleep(1)
return 1
async def worker_2():
await asyncio.sleep(2)
return 2 / 0
async def worker_3():
await asyncio.sleep(3)
return 3
async def main():
task_1 = asyncio.create_task(worker_1())
task_2 = asyncio.create_task(worker_2())
task_3 = asyncio.create_task(worker_3())
await asyncio.sleep(2)
task_3.cancel()
res = await asyncio.gather(task_1, task_2, tacansk_3, return_exceptions=True)
print(res)
if __name__ == '__main__':
asyncio.run(main())
假设任务只有两个状态:一是预备状态;二是等待状态。所谓预备状态,是指任务目前空闲,但随时待命准备运行。而等待状态,是指任务已经运行,而正在等待外部的操作完成,比如I/O操作
在这种情况下,event loop会维护两个任务列表,分别对应这两种状态;并且选取预备状态的一个任务,使其运行,一直到把这个任务把控制权交还给event loop为止
当任务把控制权交还给event loop时,event loop会根据其是否完成,把任务放到预备或等待状态的列表,然后遍历等待状态列表的任务,查看它们是否完成,如果完成,则将其放到预备状态的列表,如果未完成,则继续放在等待状态的列表,如此周而复始,直到所有的任务都完成
如果是I/O heavy,并且I/O操作很慢,需要很多任务/线程协同运行,那么使用Asyncio更合适;
如果是I/O heavy,但是I/O操作很快,只需要有限数量的任务/线程,那么使用多线程就可以
如果是CPU heavy,则可以使用多进程来解决问题