Python 3.7 提供了新的基于 asyncio 和 async / await 的方法。
- 说并发,你肯定想到了多线程 / 多进程模型,没错,多线程 / 多进程,正是解决并发问题的经典模型之一。
- 但协程是实现并发编程的一种方式。
- 事件循环启动一个统一的调度器,让调度器来决定一个时刻去运行哪个任务。
1、一个爬虫例子
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)
main(['url_1', 'url_2', 'url_3', 'url_4'])
但你仔细一算,它也占用了不少时间,五个页面分别用了 1 秒到 4 秒的时间,加起来一共用了 10 秒。
2、我们就来看看使用协程怎么写。
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):
for url in urls:
await crawl_page(url)
asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
- await 是同步调用,因此, crawl_page(url) 在当前的调用结束之前,是不会触发下一次调用的。于是,这个代码效果就和上面完全一样了,相当于我们用异步接口写了个同步代码。
- async 修饰词声明异步函数,于是,这里的 crawl_page 和 main 都变成了异步函数。
- 而调用异步函数,我们便可得到一个协程对象(coroutine object)。
3、再来说说协程的执行
执行协程有多种方法,这里我介绍一下常用的三种。
- 我们可以通过 await 来调用。await 执行的效果,和 Python 正常执行是一样的,也就是说程序会阻塞在这里,进入被调用的协程函数,执行完毕返回后再继续,而这也是 await 的字面意思。(同步调用)
- 我们可以通过 asyncio.create_task() 来创建任务
- 我们需要 asyncio.run 来触发运行。
4、协程中的一个重要概念,任务(Task)
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
asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
########## 输出 ##########
Wall time: 3.99 s
- 我们有了协程对象后,便可以通过 asyncio.create_task 来创建任务。
- 任务创建后很快就会被调度执行,这样,我们的代码也不会阻塞在任务这里。
- 所以,我们要等所有任务都结束才行,用for task in tasks: await task 即可。
5、解密协程运行时
import asyncio
async def worker_1():
print('worker_1 start')
await asyncio.sleep(1)
print('worker_1 done')
async def worker_2():
print('worker_2 start')
await asyncio.sleep(2)
print('worker_2 done')
async def main():
print('before await')
await worker_1()
print('awaited worker_1')
await worker_2()
print('awaited worker_2')
asyncio.run(main())
########## 输出 ##########
before await
worker_1 start
worker_1 done
awaited worker_1
worker_2 start
worker_2 done
awaited worker_2
Wall time: 3 s
第二个代码
import asyncio
# asyncio.create_task() 创建任务
# asyncio.sleep()在这里休息若干秒
# asyncio.run()主程序的入口函数
# async修饰词声明异步函数
# await进入被调用的协程函数,执行完毕返回后再继续
# 4 worker_1 开始运行,运行 print 输出'worker_1 start'
async def worker_1():
print('worker_1 start')
# 5 然后运行到 await asyncio.sleep(1), 从当前任务切出,事件调度器开始调度 worker_2
await asyncio.sleep(1)
# 8 一秒钟后,worker_1 的 sleep 完成,
# 事件调度器将控制权重新传给 task_1,输出 'worker_1 done',task_1 完成任务,从事件循环中退出;
print('worker_1 done')
# 6 orker_2 开始运行,运行 print 输出 'worker_2 start'
async def worker_2():
print('worker_2 start')
# 7 然后运行 await asyncio.sleep(2) 从当前任务切出
await asyncio.sleep(2)
# 10 两秒钟后,worker_2 的 sleep 完成,事件调度器将控制权重新传给 task_2,
# 输出 'worker_2 done',task_2 完成任务,从事件循环中退出;
print('worker_2 done')
async def main():
# 2 task1 和 task2 任务被创建
task1 = asyncio.create_task(worker_1())
task2 = asyncio.create_task(worker_2())
print('before await')
# 3 选择从当前的主任务中切出,事件调度器开始调度 worker_1
await task1
print('awaited worker_1')
# 9 await task1 完成,事件调度器将控制器传给主任务,输出 'awaited worker_1',
# 然后在 await task2 处继续等待;
await task2
print('awaited worker_2')
# 11 主任务输出 'awaited worker_2',协程全任务结束,事件循环结束。
# 1事件循环开启
asyncio.run(main())
- asyncio.run(main()),程序进入 main() 函数,事件循环开启;
- task1 和 task2 任务被创建,并进入事件循环等待运行;
- 运行到 print,输出 'before await';await task1 执行,用户选择从当前的主任务中切出,事件调度器开始调度 worker_1;
- worker_1 开始运行,运行 print 输出'worker_1 start',然后运行到 await asyncio.sleep(1), 从当前任务切出,事件调度器开始调度 worker_2;
- worker_2 开始运行,运行 print 输出 'worker_2 start',然后运行 await asyncio.sleep(2) 从当前任务切出;
- 以上所有事件的运行时间,都应该在 1ms 到 10ms 之间,甚至可能更短,事件调度器从这个时候开始暂停调度;
- 一秒钟后,worker_1 的 sleep 完成,事件调度器将控制权重新传给 task_1,输出 'worker_1 done',task_1 完成任务,从事件循环中退出;
- await task1 完成,事件调度器将控制器传给主任务,输出 'awaited worker_1',·然后在 await task2 处继续等待;
- 两秒钟后,worker_2 的 sleep 完成,事件调度器将控制权重新传给 task_2,输出 'worker_2 done',task_2 完成任务,从事件循环中退出;
- 主任务输出 'awaited worker_2',协程全任务结束,事件循环结束。
协成里面重要的是一个关键字await的理解,async表示其修饰的是协程任务即task,await表示的是当线程执行到这一句,此时该task在此处挂起,然后调度器去执行其他的task,当这个挂起的部分处理完,会调用回掉函数告诉调度器我已经执行完了,那么调度器就返回来处理这个task的余下语句。