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 即可。

posted on 2020-03-28 09:21  simple_孙  阅读(183)  评论(0)    收藏  举报

导航