深刻理解协程(二):yield from实现异步协程

原创不易,转载请联系做者python

深刻理解协程分为三部分进行讲解:并发

  • 协程的引入
  • yield from实现异步协程
  • async/await实现异步协程

本篇为深刻理解协程系列文章的第二篇。框架

yield from

yield from是Python3.3(PEP 380)引入的新语法。主要用于解决在生成器中不方便使用生成器的问题。主要有两个功能。异步

第一个功能:让嵌套生成器没必要再经过循环迭代yield,而能够直接使用yield fromasync

看一段代码:函数

titles = ['Python', 'Java', 'C++']
def func1(titles):
    yield titles

def func2(titles):
    yield from titles

for title in func1(titles):
    print(title)

for title in func2(titles):
    print(title)
    
# 输出结果
['Python', 'Java', 'C++']
Python
Java
C++

yield返回的完整的titles列表,而yield from返回的是列表中的具体元素。yield from能够看做是for title in titles: yield title的缩写。这样就能够用yield from减小了一次循环。oop

第二个功能:打开双向通道,把最外层给调用方与最内层的子生成器连接起来,两者能够直接通讯。.net

第二个功能听起来就让人头大。咱们再举一个例子进行说明:线程

【举个例子】:经过生成器实现整数相加,经过send()函数想生成器中传入要相加的数字,最后传入None结束相加。total保存结果。code

def generator_1():      # 子生成器
    total = 0
    while True:
        x = yield       # 解释4
        print(f'+ {x}')
        if not x:
            break
        total += x
    return total        # 解释5

def generator_2():      # 委托生成器
    while True:
        total = yield from generator_1()    # 解释3
        print(f'total: {total}')

if __name__ == '__main__':      # 调用方
    g2 = generator_2()      # 解释1
    g2.send(None)           # 解释2
    g2.send(2)              # 解释6
    g2.send(3)              
    g2.send(None)           # 解释7

# 输出结果
+ 2
+ 3
+ None
total: 5

说明

解释1g2是调用generator_2()获得的生成器对象,做为协程使用。

解释2:预激活协程g2

解释3generator_2接收的值都会通过yield from处理,经过管道传入generator_1实例。generator_2会在yield from处暂停,等待generator_1实例传回的值赋值给total

解释4:调用方传入的值都会传到这里。

解释5:此处返回的total正是generator_2()中解释3处等待返回的值。

解释6:传入2进行计算。

解释7:在计算的结尾传入None,跳出generator_1()的循环,结束计算。

说到这里,相信看过《深刻理解协程(一):协程的引入》的朋友应该就容易理解上面这段代码的运行流程了。

借助上面例子,说明一下随yield from一块儿引入的3个概念:

  • 子生成器

    yield from获取任务并完成具体实现的生成器。

  • 委派生成器

    包含有 yield from表达式的生成器函数。负责给子生成器委派任务。

  • 调用方

    指调用委派生成器的客户端代码。

在每次调用send(value)时,value不是传递给委派生成器,而是借助yield fromvalue传递给了子生成器的yield

结合asyncio实现异步协程

asyncio是Python 3.4 试验性引入的异步I/O框架(PEP 3156),提供了基于协程作异步I/O编写单线程并发代码的基础设施。其核心组件有事件循环(Event Loop)、协程(Coroutine)、任务(Task)、将来对象(Future)以及其余一些扩充和辅助性质的模块。

在引入asyncio的时候,还提供了一个装饰器@asyncio.coroutine用于装饰使用了yield from的函数,以标记其为协程。

在实现异步协程以前,咱们先看一个同步的案例:

import time
def taskIO_1():
    print('开始运行IO任务1...')
    time.sleep(2)  # 假设该任务耗时2s
    print('IO任务1已完成,耗时2s')
def taskIO_2():
    print('开始运行IO任务2...')
    time.sleep(3)  # 假设该任务耗时3s
    print('IO任务2已完成,耗时3s')

start = time.time()
taskIO_1()
taskIO_2()
print('全部IO任务总耗时%.5f秒' % float(time.time()-start))
# 输出结果
开始运行IO任务1...
IO任务1已完成,耗时2s
开始运行IO任务2...
IO任务2已完成,耗时3s
全部IO任务总耗时5.00094秒

能够看到,使用同步的方式实现多个IO任务的时间是分别执行这两个IO任务时间的总和。

下面咱们使用yield fromasyncio将上面的同步代码改为异步的。修改结果以下:

import time
import asyncio

@asyncio.coroutine # 解释1
def taskIO_1():
    print('开始运行IO任务1...')
    yield from asyncio.sleep(2)  # 解释2
    print('IO任务1已完成,耗时2s')
    return taskIO_1.__name__

@asyncio.coroutine 
def taskIO_2():
    print('开始运行IO任务2...')
    yield from asyncio.sleep(3)  # 假设该任务耗时3s
    print('IO任务2已完成,耗时3s')
    return taskIO_2.__name__

@asyncio.coroutine 
def main(): # 调用方
    tasks = [taskIO_1(), taskIO_2()]  # 把全部任务添加到task中
    done,pending = yield from asyncio.wait(tasks) # 子生成器
    for r in done: # done和pending都是一个任务,因此返回结果须要逐个调用result()
        print('协程无序返回值:'+r.result())

if __name__ == '__main__':
    start = time.time()
    loop = asyncio.get_event_loop() # 建立一个事件循环对象loop
    try:
        loop.run_until_complete(main()) # 完成事件循环,直到最后一个任务结束
    finally:
        loop.close() # 结束事件循环
    print('全部IO任务总耗时%.5f秒' % float(time.time()-start))
    
# 输出结果
开始运行IO任务2...
开始运行IO任务1...
IO任务1已完成,耗时2s
IO任务2已完成,耗时3s
协程无序返回值:taskIO_1
协程无序返回值:taskIO_2
全部IO任务总耗时3.00303秒

说明

解释1@asyncio.coroutine装饰器是协程函数的标志,咱们须要在每个任务函数前加这个装饰器,并在函数中使用yield from

解释2:此处假设该任务运行须要2秒,此处使用异步等待2秒asyncio.sleep(2),而非同步等待time.sleep(2)

执行过程

  1. 先经过get_event_loop()获取了一个标准事件循环loop(由于是一个,因此协程是单线程)
  2. 而后,咱们经过run_until_complete(main())来运行协程(此处把调用方协程main()做为参数,调用方负责调用其余委托生成器),run_until_complete的特色就像该函数的名字,直到循环事件的全部事件都处理完才能完整结束.
  3. 进入调用方协程,咱们把多个任务[taskIO_1()和taskIO_2()]放到一个task列表中,可理解为打包任务。
  4. 咱们使用asyncio.wait(tasks)来获取一个awaitable objects便可等待对象的集合,经过yield from返回一个包含(done, pending)的元组,done表示已完成的任务列表,pending表示未完成的任务列表。
  5. 由于done里面有咱们须要的返回结果,但它目前仍是个任务列表,因此要取出返回的结果值,咱们遍历它并逐个调用result()取出结果便可。
  6. 最后咱们经过loop.close()关闭事件循环。

可见,经过使用协程,极大提升了多任务的执行效率,程序最后消耗的时间是任务队列中耗时最多时间任务的时长。

总结

本篇讲述了:

  • yield from如何实现协程
  • 如何结合asyncio实现异步协程

虽然有了yield from的存在,让协程实现比以前容易了,可是这种异步协程的实现方式,并非很pythonic。如今已经不推荐使用了。下篇将与您分享更加完善的Python异步实现方式——async/await实现异步协程

参考

Python异步IO之协程(一):从yield from到async的使用

关注公众号西加加先生一块儿玩转Python
在这里插入图片描述

相关文章
相关标签/搜索