• Python——协程(Coroutine),异步IO


    目录

    生成器(Generator)

    yield表达式的使用

    生产者和消费者模型

    ​编辑

     yield from表达式

    协程(Coroutine)

    @asyncio.coroutine

    async/await

    总结


    由于GIL的存在,导致Python多线程性能甚至比单线程更糟。

     于是出现了协程(Coroutine)这么个东西。

    协程由于由程序主动控制切换,没有线程切换的开销,所以执行效率极高。对于IO密集型任务非常适用,如果是cpu密集型,推荐多进程+协程的方式。

    在Python3.4之前,官方没有对协程的支持,存在一些三方库的实现,比如gevent和Tornado。3.4之后就内置了asyncio标准库,官方真正实现了协程这一特性。

    而Python对协程的支持,是通过Generator实现的,协程是遵循某些规则的生成器。因此,我们在了解协程之前,我们先要学习生成器。

    生成器(Generator)

    我们这里主要讨论yieldyield from这两个表达式,这两个表达式和协程的实现息息相关。

    • Python2.5中引入yield表达式,参见PEP342
    • Python3.3中增加yield from语法,参见PEP380

    方法中包含yield表达式后,Python会将其视作generator对象,不再是普通的方法。

    yield表达式的使用

    我们先来看该表达式的具体使用:

    1. def test():
    2. print("generator start")
    3. n = 1
    4. while True:
    5. yield_expression_value = yield n
    6. print("yield_expression_value = %d" % yield_expression_value)
    7. n += 1
    8. # ①创建generator对象
    9. generator = test()
    10. print(type(generator))
    11. print("\n---------------\n")
    12. # ②启动generator
    13. next_result = generator.__next__()
    14. print("next_result = %d" % next_result)
    15. print("\n---------------\n")
    16. # ③发送值给yield表达式
    17. send_result = generator.send(666)
    18. print("send_result = %d" % send_result)

    执行结果:

    1. <class 'generator'>
    2. ---------------
    3. generator start
    4. next_result = 1
    5. ---------------
    6. yield_expression_value = 666
    7. send_result = 2

    方法说明:

    • __next__()方法: 作用是启动或者恢复generator的执行,相当于send(None)

    • send(value)方法:作用是发送值给yield表达式。启动generator则是调用send(None)

    执行结果的说明:

    • ①创建generator对象:包含yield表达式的函数将不再是一个函数,调用之后将会返回generator对象

    • ②启动generator:使用生成器之前需要先调用__next__或者send(None),否则将报错。启动generator后,代码将执行到yield出现的位置,也就是执行到yield n,然后将n传递到generator.__next__()这行的返回值。(注意,生成器执行到yield n后将暂停在这里,直到下一次生成器被启动)

    • ③发送值给yield表达式:调用send方法可以发送值给yield表达式,同时恢复生成器的执行。生成器从上次中断的位置继续向下执行,然后遇到下一个yield,生成器再次暂停,切换到主函数打印出send_result。

    理解这个demo的关键是:生成器启动或恢复执行一次,将会在yield处暂停。上面的第②步仅仅执行到了yield n,并没有执行到赋值语句,到了第③步,生成器恢复执行才给yield_expression_value赋值。

    生产者和消费者模型

    上面的例子中,代码中断-->切换执行,体现出了协程的部分特点。

    我们再举一个生产者、消费者的例子

    1. def consumer():
    2. print("[CONSUMER] start")
    3. r = 'start'
    4. while True:
    5. n = yield r
    6. if not n:
    7. print("n is empty")
    8. continue
    9. print("[CONSUMER] Consumer is consuming %s" % n)
    10. r = "200 ok"
    11. def producer(c):
    12. # 启动generator
    13. start_value = c.send(None)
    14. print(start_value)
    15. n = 0
    16. while n < 3:
    17. n += 1
    18. print("[PRODUCER] Producer is producing %d" % n)
    19. r = c.send(n)
    20. print('[PRODUCER] Consumer return: %s' % r)
    21. # 关闭generator
    22. c.close()
    23. # 创建生成器
    24. c = consumer()
    25. # 传入generator
    26. producer(c)

     执行结果:

    1. [CONSUMER] start
    2. start
    3. [PRODUCER] producer is producing 1
    4. [CONSUMER] consumer is consuming 1
    5. [PRODUCER] Consumer return: 200 ok
    6. [PRODUCER] producer is producing 2
    7. [CONSUMER] consumer is consuming 2
    8. [PRODUCER] Consumer return: 200 ok
    9. [PRODUCER] producer is producing 3
    10. [CONSUMER] consumer is consuming 3
    11. [PRODUCER] Consumer return: 200 ok

      

     

     

     yield from表达式

    Python3.3版本新增yield from语法,新语法用于将一个生成器部分操作委托给另一个生成器。此外,允许子生成器(即yield from后的“参数”)返回一个值,该值可供委派生成器(即包含yield from的生成器)使用。并且在委派生成器中,可对子生成器进行优化。

    我们先来看最简单的应用,例如:

    1. # 子生成器
    2. def test(n):
    3. i = 0
    4. while i < n:
    5. yield i
    6. i += 1
    7. # 委派生成器
    8. def test_yield_from(n):
    9. print("test_yield_from start")
    10. yield from test(n)
    11. print("test_yield_from end")
    12. for i in test_yield_from(3):
    13. print(i)

    输出:

    1. test_yield_from start
    2. 0
    3. 1
    4. 2
    5. test_yield_from end

    这里我们仅仅给这个生成器添加了一些打印,如果是正式的代码中,你可以添加正常的执行逻辑。

    如果上面的test_yield_from函数中有两个yield from语句,将串行执行。比如将上面的test_yield_from函数改写成这样:

    1. def test_yield_from(n):
    2. print("test_yield_from start")
    3. yield from test(n)
    4. print("test_yield_from doing")
    5. yield from test(n)
    6. print("test_yield_from end")

    将输出:

    1. test_yield_from start
    2. 0
    3. 1
    4. 2
    5. test_yield_from doing
    6. 0
    7. 1
    8. 2
    9. test_yield_from end

    在这里,yield from起到的作用相当于下面写法的简写形式

    1. for item in test(n):
    2. yield item

    看起来这个yield from也没做什么大不了的事,其实它还帮我们处理了异常之类的。具体可以看stackoverflow上的这个问题:In practice, what are the main uses for the new “yield from” syntax in Python 3.3?

    协程(Coroutine)

    • Python3.4开始,新增了asyncio相关的API,语法使用@asyncio.coroutineyield from实现协程
    • Python3.5中引入async/await语法,参见PEP492

    我们先来看Python3.4的实现。

    @asyncio.coroutine

    Python3.4中,使用@asyncio.coroutine装饰的函数称为协程。不过没有从语法层面进行严格约束。

    对于Python原生支持的协程来说,Python对协程和生成器做了一些区分,便于消除这两个不同但相关的概念的歧义:

    • 标记了@asyncio.coroutine装饰器的函数称为协程函数,iscoroutinefunction()方法返回True
    • 调用协程函数返回的对象称为协程对象,iscoroutine()函数返回True

    举个栗子,我们给上面yield from的demo中添加@asyncio.coroutine

    1. import asyncio
    2. ...
    3. @asyncio.coroutine
    4. def test_yield_from(n):
    5. ...
    6. # 是否是协程函数
    7. print(asyncio.iscoroutinefunction(test_yield_from))
    8. # 是否是协程对象
    9. print(asyncio.iscoroutine(test_yield_from(3)))

    毫无疑问输出结果是True。

    可以看下@asyncio.coroutine的源码中查看其做了什么,我将其源码简化下,大致如下:

    1. import functools
    2. import types
    3. import inspect
    4. def coroutine(func):
    5. # 判断是否是生成器
    6. if inspect.isgeneratorfunction(func):
    7. coro = func
    8. else:
    9. # 将普通函数变成generator
    10. @functools.wraps(func)
    11. def coro(*args, **kw):
    12. res = func(*args, **kw)
    13. res = yield from res
    14. return res
    15. # 将generator转换成coroutine
    16. wrapper = types.coroutine(coro)
    17. # For iscoroutinefunction().
    18. wrapper._is_coroutine = True
    19. return wrapper

    将这个装饰器标记在一个生成器上,就会将其转换成coroutine。

    然后,我们来实际使用下@asyncio.coroutineyield from

    1. import asyncio
    2. @asyncio.coroutine
    3. def compute(x, y):
    4. print("Compute %s + %s ..." % (x, y))
    5. yield from asyncio.sleep(1.0)
    6. return x + y
    7. @asyncio.coroutine
    8. def print_sum(x, y):
    9. result = yield from compute(x, y)
    10. print("%s + %s = %s" % (x, y, result))
    11. loop = asyncio.get_event_loop()
    12. print("start")
    13. # 中断调用,直到协程执行结束
    14. loop.run_until_complete(print_sum(1, 2))
    15. print("end")
    16. loop.close()

    执行结果:

    1. start
    2. Compute 1 + 2 ...
    3. 1 + 2 = 3
    4. end

    print_sum这个协程中调用了子协程compute,它将等待compute执行结束才返回结果。

    这个demo点调用流程如下图:

    EventLoop将会把print_sum封装成Task对象

    流程图展示了这个demo的控制流程,不过没有展示其全部细节。比如其中“暂停”的1s,实际上创建了一个future对象, 然后通过BaseEventLoop.call_later()在1s后唤醒这个任务。

    值得注意的是,@asyncio.coroutine将在Python3.10版本中移除。

    async/await

    Python3.5开始引入async/await语法(PEP 492),用来简化协程的使用并且便于理解。

    async/await实际上只是@asyncio.coroutineyield from的语法糖:

    • @asyncio.coroutine替换为async
    • yield from替换为await

    即可。

    比如上面的例子:

    1. import asyncio
    2. async def compute(x, y):
    3. print("Compute %s + %s ..." % (x, y))
    4. await asyncio.sleep(1.0)
    5. return x + y
    6. async def print_sum(x, y):
    7. result = await compute(x, y)
    8. print("%s + %s = %s" % (x, y, result))
    9. loop = asyncio.get_event_loop()
    10. print("start")
    11. loop.run_until_complete(print_sum(1, 2))
    12. print("end")
    13. loop.close()

    我们再来看一个asyncio中Future的例子:

    1. import asyncio
    2. future = asyncio.Future()
    3. async def coro1():
    4. print("wait 1 second")
    5. await asyncio.sleep(1)
    6. print("set_result")
    7. future.set_result('data')
    8. async def coro2():
    9. result = await future
    10. print(result)
    11. loop = asyncio.get_event_loop()
    12. loop.run_until_complete(asyncio.wait([
    13. coro1()
    14. coro2()
    15. ]))
    16. loop.close()

    输出结果:

    1. wait 1 second
    2. (大约等待1秒)
    3. set_result
    4. data

    这里await后面跟随的future对象,协程中yield from或者await后面可以调用future对象,其作用是:暂停协程,直到future执行结束或者返回result或抛出异常。

    而在我们的例子中,await future必须要等待future.set_result('data')后才能够结束。将coro2()作为第二个协程可能体现得不够明显,可以将协程的调用改成这样:

    1. loop = asyncio.get_event_loop()
    2. loop.run_until_complete(asyncio.wait([
    3. # coro1(),
    4. coro2(),
    5. coro1()
    6. ]))
    7. loop.close()

    输出的结果仍旧与上面相同。

    其实,async这个关键字的用法不止能用在函数上,还有async with异步上下文管理器,async for异步迭代器. 对这些感兴趣且觉得有用的可以网上找找资料,这里限于篇幅就不过多展开了。

    总结

    本文就生成器和协程做了一些学习、探究和总结,不过并没有做过多深入的研究。权且作为入门到一个笔记,之后将会尝试自己实现一下异步API,希望有助于理解学习。

  • 相关阅读:
    FBZP 维护支持程序 & 创建国家付款方式
    torch.cuda
    Hadoop3教程(三十五):(生产调优篇)HDFS小文件优化与MR集群简单压测
    研发过程中的文档管理与工具
    ELK-日志服务【es-安装使用】
    12-k8s-HPA自动扩缩容
    aijs 对象排序
    超实用的Go语言基础教程,让你快速上手刷题!!
    R语言和医学统计学(8):logistic回归
    Stable Diffusion WebUI详细使用指南
  • 原文地址:https://blog.csdn.net/m0_56134806/article/details/128187126