python-协程
Python2
yield
yield出现的地方都是生成器,包含yield的函数不再是一个函数,是一个生成器,直接调用这样的函数什么都不会都执行,只能通过send方法(以前也可用next方法,但现在被删掉了)或迭代执行
每调用一次send函数,就会开始执行,直到碰到yield语句停止,本次send函数的返回值就是yield后面表达式值,而yield表达式的返回值就是send传进来的参数
Python2.5以前,yield是一个语句,但现在2.5中,yield是一个表达式(Expression)
def h(): print 'Wen Chuan', m = yield 5 # Fighting! print m d = yield 12 print 'We are together!' c = h() m = c.send(None) #m 获取了yield 5 的参数值 5,第一次调用send参数必须是None,否则就会报错 d = c.send('Fighting!') #d 获取了yield 12 的参数值12 print 'We will never forget the date', m, '.', d
输出结果:
>>> fib(6)
<generator object fib at 0x104feaaa0>
gen.coroutine的应用
直接上官网的例子
普通的回调函数的方式:
class AsyncHandler(RequestHandler): @asynchronous def get(self): http_client = AsyncHTTPClient() http_client.fetch("http://example.com", callback=self.on_fetch) def on_fetch(self, response): do_something_with_response(response) self.render("template.html")
上面的坏处是第一不直观,第二可能造成循环的嵌套,书写麻烦,可能存在on_fetch函数中再次调用回调函数的情况
同步的方式
class GenAsyncHandler(RequestHandler): @gen.coroutine def get(self): http_client = AsyncHTTPClient() response = yield http_client.fetch("http://example.com") do_something_with_response(response) self.render("template.html")
可以看出代码明显清晰,简单多了。如果有深层次的回调,效果会更明显。
@gen.coroutine现在用来代替版本3.0之前的@gen.engine
关于协程的实现,请参考
http://blog.csdn.net/wyx819/article/details/45420017
Python3
使用生成器,是 Python 2 开头的时代实现协程的老方法了,Python 3.7 提供了新的基于 asyncio 和 async / await 的方法。
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
async 修饰词声明异步函数,于是,这里的 crawl_page 和 main 都变成了异步函数。而调用异步函数,我们便可得到一个协程对象(coroutine object)。举个例子,如果你 print(crawl_page('')),便会输出<coroutine object crawl_page at 0x000002BEDF141148>,提示你这是一个 Python 的协程对象,而并不会真正执行这个函数。
执行方法
执行协程有多种方法,这里我介绍一下常用的三种。
可以通过 await 来调用。
await 执行的效果,和 Python 正常执行是一样的,也就是说程序会阻塞在这里,进入被调用的协程函数,执行完毕返回后再继续,而这也是 await 的字面意思。代码中 await asyncio.sleep(sleep_time) 会在这里休息若干秒,await crawl_page(url) 则会执行 crawl_page() 函数。
开发者要提前知道一个任务的哪个环节会造成I/O阻塞,然后把这个环节的代码异步化处理,并且通过await来标识在任务的该环节中断该任务执行,从而去执行下一个事件循环任务。这样可以充分利用CPU资源,避免CPU等待I/O造成CPU资源白白浪费。当之前任务的那个环节的I/O完成后,线程可以从await获取返回值,然后继续执行没有完成的剩余代码。
需要 asyncio.run 来触发运行。
asyncio.run 这个函数是 Python 3.7 之后才有的特性,可以让 Python 的协程接口变得非常简单,你不用去理会事件循环怎么定义和怎么使用的问题(我们会在下面讲)。一个非常好的编程规范是,asyncio.run(main()) 作为主程序的入口函数,在程序运行周期内,只调用一次 asyncio.run。
通过 asyncio.create_task() 来创建任务
发现上面代码的耗时是串行的10s,await 是同步调用,因此, crawl_page(url) 在当前的调用结束之前,是不会触发下一次调用的。相当于我们用异步接口写了个同步代码。
可以通过创建任务进行异步执行,代码如下:
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
craw_page方法代码在create_task创建任务后就开始调度执行,await的作用是等待每个任务执行完毕后main方法再返回。
除了上面for...in等待任务执行完成,还可以通过下面的方式:
await asyncio.gather(*tasks)
如果我们想给某些协程任务限定运行时间,一旦超时就取消,又该怎么做呢?再进一步,如果某些协程运行时出现错误,又该怎么处理呢?同样的,来看代码。
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
不过要注意return_exceptions=True这行代码。如果不设置这个参数,错误就会完整地 throw 到我们这个执行层,从而需要 try except 来捕捉,这也就意味着其他还没被执行的任务会被全部取消掉。为了避免这个局面,我们将 return_exceptions 设置为 True 即可。
浙公网安备 33010602011771号