协程是实现并发编程的一种方式
实例
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)
%time main(['url_1', 'url_2', 'url_3', 'url_4'])
########## 输出 ##########
crawling url_1
OK url_1
crawling url_2
OK url_2
crawling url_3
OK url_3
crawling url_4
OK url_4
Wall time: 10 s
这是一个简单的爬虫,main()函数执行时,调取crawl_page()函数进行网络通信,经过若干秒等待后收到结果,然后执行下一个。
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)
%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
########## 输出 ##########
crawling url_1
OK url_1
crawling url_2
OK url_2
crawling url_3
OK url_3
crawling url_4
OK url_4
Wall time: 10 s
首先import asyncio,这个库包含了大部分实现协程所需的魔法工具。
async修饰词声明异步函数,这里的crawl_page和main都变成了异步函数,而调用异步函数,便可以得到一个协程对象。
例,如果print(crawl_page(’’)),便会输出
协程的执行
首先,可以通过await来调用。await执行的效果,和Python正常执行是一样的,也就是说程序会阻塞在这里,进入被调用的协程函数,执行完毕返回后再继续,而这也是await的字面意思。
代码中await asyncio.sllep(sleep_tiem)
会在这里休息若干秒,await crawl_page(url)
则会执行crawl_page()
函数。
其次,可以通过asyncio.create_task()来创建任务。
最后,需要asyncio.run来触发运行。asyncio.run这个函数是Python3.7之后才有的特性。
运行结果还是10秒,是因为await是同步调用,因此,crawl_page(url)再当前的调用结束之前,是不会触发下一次调用。
协程的重要概念:任务(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
%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
########## 输出 ##########
crawling url_1
crawling url_2
crawling url_3
crawling url_4
OK url_1
OK url_2
OK url_3
OK url_4
Wall time: 3.99 s
有了协程对象后,便可以通过asyncio.creat_task来创建任务。
任务创建后很快就会被调度执行,这样,代码也不会阻塞再任务这里。
执行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]
await asyncio.gather(*tasks)
%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
########## 输出 ##########
crawling url_1
crawling url_2
crawling url_3
crawling url_4
OK url_1
OK url_2
OK url_3
OK url_4
Wall time: 4.01 s
*task
解包列表,将列表变成了函数的参数;与之对的是,**dict将字典变成了函数的参数。
注:asyncio.create_task,asyncio.run都是Python3.7以上的版本才提供的。
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')
%time 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
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():
task1 = asyncio.create_task(worker_1())
task2 = asyncio.create_task(worker_2())
print('before await')
await task1
print('awaited worker_1')
await task2
print('awaited worker_2')
%time asyncio.run(main())
########## 输出 ##########
before await
worker_1 start
worker_2 start
worker_1 done
awaited worker_1
worker_2 done
awaited worker_2
Wall time: 2.01 s
过程分析
1.asyncio.run(main()),程序进入main()函数,事件循环开启;
2.task1和task2任务被创建,并进入时间循环等待运行;运行到print,输出’before await’;
3.await task1执行,用户选择从当前的主任务切出,时间调度器开始调度worker_1;
4.worker_1开始运行,运行print输出’worker_1 start‘,然后运行到await asyncio.sleep(1),从当前任务切出,时间调度器开始调度worker_2;
5.worker_2开始运行,运行print输出’worker_2 start’,然后运行await asyncio.sleep(2)从当前任务切出;
6.以上所有时间的运行时间,都应该再1ms到10ms之间,甚至可能更短,时间调度器从这个时候开始暂停调度;
7.一秒钟后,worker_1的sleep完成,时间调度器将控制权重新传给task_1,输出’worker_1 done’,task_1完成任务,从事件循环中退出;
8.await task1完成,时间调度器将控制器传给主任务,输出’awaiter worker_1’,然后在await task2处继续等待;
9.两秒中后,worker_2的sleep完成,事件调度器将控制权重新传给task_2,输出’worker_2 done’,task_2完成任务,从事件循环中退出;
10,主任务输出’awaited worker_2’,协程全任务结束,事件循环结束。
示例
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, task_3, return_exceptions=True)
print(res)
%time asyncio.run(main())
########## 输出 ##########
[1, ZeroDivisionError('division by zero'), CancelledError()]
Wall time: 2 s
worker_1正常运行 ,worker_2运行中出现错误,worker_3执行时间过长被cancel掉,这些信息会全部体现在最终的返回结果res中。
需要注意的是,return_exceptions=True这行代码。如果不设置这个参数,错误就会完整地throw到执行层,从而需要try except来捕捉,这也意味着其他还没有被执行的任务全部取消掉。
为了避免这个情况,将return_exceptions设置为True即可。
协程和多线程的区别,主要在于两点,一是协程为单线程;二是协程由用户决定,在哪些地方交出控制权,切换到下一个任务。
协程的写法更加简洁清晰
写协程程序的时候,要有清晰的事件循环概念,知道程序在什么时候需要暂停、等待I/O,什么时候需要一并执行到底。