python系列7:python中的协程

协程的功能和多线程、多进程类似,将同步代码变为异步,同时开销最小,性能最佳。但是事实上还是一个线程,因此只适用于异步io,比如下载东西、运行GPU等。可以参考这篇文章:https://realpython.com/async-io-python/#async-io-design-patterns

本文思路:
生成器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版本重要更新】

参考文档:https://docs.python.org/zh-cn/3/library/asyncio-task.html
这里总结一下:

  • 运行方式:
    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())

可以设置buffer_size。默认的buffer_size都是1。耗时越多的环节,我们设置的buffer_size越可以越多,
it = buffer_iterable(gen(), buffer_size=2)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值