生成器generator:类似断点调试,但是运行到程序结束会报错。原理是用yield挂起,执行run/execute/next/send等代码时再向下运行,next/send方法类似调试时点击debug按钮,并且输入一个参数(n),跑完后的结果返回给r。
委托生成器(yield from 生成器/异步函数,等价于await) ,和生成器作用类似,但是运行到程序结束不会报错,而是直接再次执行生成器。
协程async=@asyncio.coroutine :也就是异步函数,在需要打断点的地方加上await
协程的执行 :运行到断点后会切换到loop页面继续执行其他任务。loop页面为:loop = asyncio.get_event_loop(); loop.run_until_complete(task); loop.close().
多任务 :loop中的task要改为asyncio.gather(tasks)或asyncio.wait(tasklist)
future ,带回调函数的协程:future = asyncio.ensure_future(异步函数),future.add_done_callback(callback)调用异步函数。callback的输入为future,其内部可以用future.result获取结果。这里也可以不使用回调,在loop complete后直接用future.result获取结果
所有的这一切,在3.7版本之后发生了巨大变化:
0.【3.7版本重要更新】
这里总结一下:
- 运行方式:
3.7以前用loop的一套
3.7以后asyncio.run(异步任务)
在jupyter notebook中使用await。 - 定义方式:使用asyncio将普通函数变为异步函数,在需要打断点的地方加上await即可
- 多任务创建
3.7以前用asyncio.gather(异步函数清单)或asyncio.wait(异步函数列表)
3.7以后用asyncio.create_task(异步函数)+await
0.1 不再需要基于生成器!!!
普通函数可以变为协程,通过return返回。
哭了,跟3.6差别也太大了。
也就是说,下面的代码是成立的:
import time
import asyncio
async def nested():
return 42
async def main():
print(await nested())
asyncio.run(main())
既然能直接获得return的值,我们也不需要future了。
0.2 去除loop,增加asyncio.run
自python3.7版本开始,不再需要起loop了,直接使用asynicio.run(task)
即可,例如3.1节的代码可以改成:
import asyncio
async def hello(name): # 将普通函数变成异步class
print('Hello,', name)
# 定义协程对象,实例化
coroutine = hello("World")
# 直接run
asyncio.run(coroutine)
0.3 多任务
多任务只需要再封装一层create_task即可:
import asyncio
import time
async def visit_url(url, response_time):
"""访问 url"""
await asyncio.sleep(response_time)
return f"访问{url}, 已得到返回结果"
async def run_task():
task = asyncio.create_task(visit_url('http://wangzhen.com', 2))
task_2 = asyncio.create_task(visit_url('http://another', 3))
await task
await task_2
start_time = time.perf_counter()
asyncio.run(run_task())
print(f"消耗时间:{time.perf_counter() - start_time}")
如果不用create_task,那将是同步运行:
当然我们也可以用gather或wait方法
下面是原始笔记:
1. 从迭代器到生成器
1.1 可迭代对象:可以for遍历
可迭代对象是拥有__iter__
方法、可以用for进行遍历的函数,比如常见的list、dict等等都是可迭代对象
1.2 迭代器:可以用next遍历
迭代器比可迭代对象多了一个__netxt__()
方法,可以不再使用for循环来间断获取元素值,而可以直接使用next()方法来获取元素值。
可以通过dir方法来查看是否包含此函数。另外,可以用iter函数,将可迭代对象转换为迭代器,如下示例
1.3 生成器:惰性next遍历,原理是yield挂起
生成器generator在迭代器的基础上,又实现了yield。生成器是惰性计算的代码,只有在执行run/execute/next/send等代码时才会真正运行,并且在运行到yield时会将后面的值返回,并堵塞住流程。
既然是惰性计算,因此生成器不会存储所有的值,而是在调用的时候进行计算(也就是用时间换空间)。
在python中有两种方式实现生成器
1)列表可以通过类似下面的方式实现:L = [x * x for x in range(1, 11) if x % 2 == 0]
把一个列表生成式的[]改成(),就创建了一个generator:G = (x * x for x in range(1, 11) if x % 2 == 0)
然后用迭代器去取出generator的元素:
for g in G:
print(g)
2)由函数推导过来,比如:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
将其中的print改为yield,这个函数就变成了一个generator:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
generator在每次调用next/send的时候进入,遇到yield的时候跳出。send方法和next方法唯一的区别是在执行send方法会首先把上一次挂起的yield语句的返回值通过参数设定。
生成器在其生命周期中,会有如下四个状态
GEN_CREATED # 等待开始执行,生成器声明后的状态
GEN_RUNNING # 解释器正在执行(只有在多线程应用中才能看到这个状态)
GEN_SUSPENDED # 在yield表达式处暂停时的状态
GEN_CLOSED # 执行结束,生成器执行close后的正太
注意用生成器是没有办法获得return值的,需要捕获StopIteration错误,返回值包含在StopIteration的value中:
F = fib(6) # 声明生成器对象
while True:
try:
print(next(F)) # 不断调用next方法
except StopIteration as e:
print('Generator return value:', e.value)
break
注意要break,不然跳不出while循环~
下面是一个综合的例子:
def consumer():
r = ''
while True:
n = yield r
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
next(c) # 注意要先调用next,执行到yield部分。当然也可以使用c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n) # 调用send,回到consumer的n = yield r这一行,继续往下执行
print('[PRODUCER] Consumer return: %s' % r)
c.close() # 在一切结束后,记得调用close关闭生成器。
c = consumer()
produce(c)
2. 协程
2.1 生成器问题:next到最后报错
生成器为我们引入了暂停函数执行(yield)的功能。当有了暂停的功能之后,人们就想能不能在生成器暂停的时候干点其他的事情,然后向其发送结果。
假如我们做一个爬虫。我们要爬取多个网页,这里简单举例两个网页(两个spider函数),获取HTML(耗IO耗时),然后再对HTML对行解析取得我们感兴趣的数据。我们希望能在get_html()这里暂停一下,不用傻乎乎地去等待网页返回,而是去做别的事。等过段时间再回过头来到刚刚暂停的地方,接收返回的html内容,然后还可以接下去解析parse_html(html)。
生成器其实已经是协程的雏形了。协程是在单线程里实现任务的切换,利用同步的方式去实现异步。此外,协程不再需要锁,提高了并发性能。
协程与生成器不同的地方在于,next到最后不会报错。
2.2 yield from:处理stopException异常
当 yield from 后面加上一个生成器后,就实现了生成的嵌套。这个函数我们叫做委托生成器。yield from帮我们做了很多while下的异常处理。在下面的例子中,通过yield from的一层包装,我们可以用while+next不断调用了。这里当子生成器遇到stopException之后,会继续往下运行(这里就是再次调用fib(maxnum))。
2.3 协程:async(本机计算) + yield from(io/网络)
下面展示了委托生成器中yield from真正的作用,一般都是放耗时的io操作;其他地方是不好时间的操作。这样在运行到yield from时,程序会挂起,继续执行其他的代码,在下面的例子里面有两个协程在竞争,会优先执行先解除挂起状态的协程。
为了模拟io阻塞,我们把斐波那契函数的大部分内容移到委托生成器中,将子生成器改成一个sleep:
import asyncio,random
@asyncio.coroutine
def smart_fib(n):
index = 0
a = 0
b = 1
while index < n:
sleep_secs = random.uniform(0, 0.2)
yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗时的操作
print('Smart one think {} secs to get {}'.format(sleep_secs, b))
a, b = b, a + b
index += 1
@asyncio.coroutine
def stupid_fib(n):
index = 0
a = 0
b = 1
while index < n:
sleep_secs = random.uniform(0, 0.4)
yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗时操作
print('Stupid one think {} secs to get {}'.format(sleep_secs, b))
a, b = b, a + b
index += 1
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [
smart_fib(10),
stupid_fib(10),
]
loop.run_until_complete(asyncio.wait(tasks))
print('All fib finished.')
loop.close()
这里的asyncio.sleep(n)是asyncio自带的工具函数,可以模拟IO阻塞,返回的是一个协程对象。结果如下:
3. asyncio库详解
3.1 async=@asyncio.coroutine,函数对象可挂起
只要在一个函数前面加上 async 关键字,函数立马就变成了一个class,并且实例化之后可以挂起。
asyncio直接内置了对异步IO的支持。asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。
下面是一个完整例子:
import asyncio
async def hello(name): # 将普通函数变成异步class
print('Hello,', name)
# 定义协程对象,实例化
coroutine = hello("World")
# 定义事件循环对象容器
loop = asyncio.get_event_loop()
# 将任务扔进事件循环对象中并触发
loop.run_until_complete(coroutine)
3.2 future: 绑定回调函数
在上面斐波那契的例子里面,挂起的函数继续执行不需要依赖外面的信息;但是如果要依赖外面的信息,我们就需要使用回调函数了。此时我们需要把coroutine再次封装成future。
future包裹了协程,为协程添加了回调模式,可以指定结果成功和失败时的回调函数。
future的状态包括:
Pending:创建future,还未执行
Running:事件循环正在调用执行任务
Done:任务执行完毕
Cancelled:Task被取消后的状态
下面的例子中,我们需要知道挂起了多少时间,因此需要用future:
import asyncio
import time
async def _sleep(x): #要获得return的值,因此后面要再封装一次
time.sleep(x)
return '暂停了{}秒!'.format(x)
coroutine = _sleep(2)
task = asyncio.ensure_future(coroutine)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
#使用task.result() 可以取得return的结果
print('返回结果:{}'.format(task.result()))
还有一种方式是使用回调函数:
import time
import asyncio
async def _sleep(x):
time.sleep(x)
return '暂停了{}秒!'.format(x)
def callback(future):
print('这里是回调函数,获取返回结果是:', future.result())
coroutine = _sleep(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
# 使用add_done_callback添加任务完成后的回调函数
task.add_done_callback(callback)
loop.run_until_complete(task)
3.3 wait/gather: 多任务
在async函数里,可以用await来代替yield from。后面不再使用yield from。
asyncio实现并发,就需要多个协程来完成任务,每当运行到await时当前协程挂起,然后其他协程继续工作。同时,需要用ensure_future将同步改为异步调度。
async def do_some_work(x):
print('Waiting: ', x)
await asyncio.sleep(x) #io协程
return 'Done after {}s'.format(x)
# 声明多个协程对象
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)
# 因为要获取return值,因此将协程转成task,并组成list
tasks = [
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]
接下来将这些协程注册到事件循环中,有两种方法
# 1. 使用asyncio.wait()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# 2. 使用asyncio.gather()
# 千万注意,这里的*不能省略
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*tasks))
# return的结果,可以用task.result()查看。
for task in tasks:
print('Task ret: ', task.result())
下面来看一下两者的区别
import asyncio
# 用于内部的协程函数
async def do_some_work(x):
print('Waiting: ', x)
await asyncio.sleep(x)
return 'Done after {}s'.format(x)
# 外部的协程函数
async def main():
# 创建三个协程对象
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)
# 将协程转为task,并组成list
tasks = [
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]
# 【重点】:await 一个task列表(协程)
# dones:表示已经完成的任务
# pendings:表示未完成的任务
dones, pendings = await asyncio.wait(tasks)
for task in dones:
print('Task ret: ', task.result())
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
如果这边,使用的是asyncio.gather(),是这么用的
#注意这边返回结果,与wait不一样
results = await asyncio.gather(*tasks)
for result in results:
print('Task ret: ', result)
简单来说,就是gather直接输出结果,而wait输出的是任务,需要调用dones的result()方法获得结果;输入方面,gather用的是可变长参数列表,而wait是list。
此外,wait可以对协程列表进行控制:
import asyncio
import random
async def coro(tag):
await asyncio.sleep(random.uniform(0.5, 5))
loop = asyncio.get_event_loop()
tasks = [coro(i) for i in range(1, 11)]
# 【控制运行任务数】:
# FIRST_COMPLETED :第一个任务完成后返回
# FIRST_EXCEPTION:产生第一个异常后返回
# ALL_COMPLETED:所有任务完成返回 (默认选项)
dones, pendings = loop.run_until_complete(
asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED))
print("第一次完成的任务数:", len(dones))
# 【控制时间】:运行一秒后,就返回
dones2, pendings2 = loop.run_until_complete(
asyncio.wait(pendings, timeout=1))
print("第二次完成的任务数:", len(dones2))
# 【默认】:所有任务完成后返回
dones3, pendings3 = loop.run_until_complete(asyncio.wait(pendings2))
print("第三次完成的任务数:", len(dones3))
loop.close()
3.4 动态添加协程
有两种方法:
1)同步方法
new_loop = asyncio.new_event_loop()
new_loop.call_soon_threadsafe(func,*args)
2)异步方法
new_loop = asyncio.new_event_loop()
asyncio.run_coroutine_threadsafe(asyncfuc(*args), new_loop)
下面是一个综合例子,注意queue.get是一个阻塞方法
Queue.queue方法这里插两句:
- 默认的
get()
是阻塞方法 -
get(False)
是非阻塞方法,但是要加上queue.Empty
异常处理 -
get_nowait()
也是一样的非阻塞方法 - 当然也可以用阻塞+过期时间
3.5 将同步变为异步
使用asyncio时,最后执行的函数必须是异步的,我们一般会用到:
- asyncio自带的sleep
- aiohttp 异步请求
- aiofile 异步文件
另一种方式是使用loop.run_in_executor(executor, func, *args) ,其中executor是线程池。
4. 进行实战
4.1 下载多个文件
4.2 生产者-消费者模型
使用master-worker的方式,master主要用户获取队列的msg,worker用户处理消息。
这里起两个线程,主线程loop_thread用来监听队列,子线程consumer_thread用于处理队列。这里使用redis的队列。
在mac上,运行brew install redis
来安装,然后运行redis-server
起服务,运行redis-cli
打开命令行界面:
代码如下:
如果继续往queue里添加数据,会继续进行消费:
4.2 异步读取多个视频/摄像头
5. pipeline
当有流水线时,我们可以用asyncio-buffered-pipeline库,让不同阶段同时进行。
原代码中,gen_2需要等待gen_1所有的任务完成才会继续执行,总时间需要30秒:
import asyncio
async def gen_1():
for value in range(0, 10): # 注意这里是一个同步的for循环!!!
await asyncio.sleep(1) # Could be a slow HTTP request
yield value
async def gen_2(it):
async for value in it:
await asyncio.sleep(1) # Could be a slow HTTP request
yield value * 2
async def gen_3(it):
async for value in it:
await asyncio.sleep(1) # Could be a slow HTTP request
yield value + 3
async def main():
it_1 = gen_1()
it_2 = gen_2(it_1)
it_3 = gen_3(it_2)
async for val in it_3:
print(val)
asyncio.run(main())
改用asyncio_buffered_pipeline后,会从batch生产模式变为流水线模式,耗时变为12秒左右:
import asyncio
from asyncio_buffered_pipeline import buffered_pipeline
async def gen_1():
for value in range(0, 10):
await asyncio.sleep(1) # Could be a slow HTTP request
yield value
async def gen_2(it):
async for value in it:
await asyncio.sleep(1) # Could be a slow HTTP request
yield value * 2
async def gen_3(it):
async for value in it:
await asyncio.sleep(1) # Could be a slow HTTP request
yield value + 3
async def main():
buffer_iterable = buffered_pipeline()
it_1 = buffer_iterable(gen_1())
it_2 = buffer_iterable(gen_2(it_1))
it_3 = buffer_iterable(gen_3(it_2))
async for val in it_3:
print(val)
asyncio.run(main())