深入理解 Python 虚拟机:协程初探——不过是生成器而已

在 Python 3.4 Python 引入了一个非常有用的特性——协程,在后续的 Python 版本当中不断的进行优化和改进,引入了新的 await 和 async 语法。在本篇文章当中我们将详细介绍一下 Python 协程的原理以及虚拟机具体的实现协程的方式。

什么是协程

Coroutines are computer program components that allow execution to be suspended and resumed, generalizing subroutines for cooperative multitasking.

根据 wiki 的描述,协程是一个允许停下来和恢复执行的程序,从文字上来看这与我们的常识或者直觉是相互违背的,因为在大多数情况下我们的函数都是执行完才返回的。其实目前 Python 当中早已有了一个特性能够做到这一点,就是生成器,如果想深入了解一下生成器的实现原理和相关的字节码可以参考这篇文章 深入理解 Python 虚拟机:生成器停止背后的魔法

现在在 Python 当中可以使用 async 语法定一个协程函数(当函数使用 async 进行修饰的时候这个函数就是协程函数),当我们调用这个函数的时候会返回一个协程对象,而不是直接调用函数:

>>> async def hello():
... return 0
...
>>> hello()
<coroutine object hello at 0x100a04740>

在 inspect 模块当中也有一个方法用于判断一个函数是否是协程函数:

import inspect

async def hello():
return 0 print(inspect.iscoroutinefunction(hello)) # True

在 Python 当中当你想创建一个协程的话,就直接使用一个 async 关键字定一个函数,调用这个函数就可以得到一个协程对象。

在协程当中可以使用 await 关键字等待其他协程完成,当被等待的协程执行完成之后,就会返回到当前协程继续执行:

import asyncio
import datetime
import time async def sleep(t):
time.sleep(t) async def hello():
print("start a coroutine", datetime.datetime.now())
await sleep(3)
print("wait for 3s", datetime.datetime.now()) if __name__ == '__main__':
coroutine = hello()
try:
coroutine.send(None)
except StopIteration:
print("coroutine finished")
start a coroutine 2023-10-15 02:21:33.503505
wait for 3s 2023-10-15 02:21:36.503984
coroutine finished

在上面的程序当中,await sleep(3) 确实等待了 3 秒之后才继续执行。

协程的实现

在 Python 当中协程其实就是生成器,只不过在生成器的基础之上稍微包装了一下,比如在写成当中的 await 语句,其实作用和 yield from 对于生成器的作用差不多,稍微有点细微差别。我们用几个例子来详细分析一下协程和生成器之间的关系:

async def hello():
return 0 if __name__ == '__main__':
coroutine = hello()
print(coroutine)
try:
coroutine.send(None)
except StopIteration:
print("coroutine finished")

上面的代码的输出结果:

<coroutine object hello at 0x1170200c0>
coroutine finished

在上面的代码当中首先调用 hello 之后返回一个协程对象,协程对象和生成器对象一样都有 send 方法,而且作用也一样都是让协程开始执行。和生成器一样当一个生成器执行完成之后会产生 StopIteration 异常,因此需要对异常进行 try catch 处理。和协程还有一个相关的异常为 StopAsyncIteration,这一点我们在之后的文章详细说。

我们再来写一个稍微复杂一点例子:

async def bar():
return "bar" async def foo():
name = await bar()
print(f"{name = }")
return "foo" if __name__ == '__main__':
coroutine = foo()
try:
coroutine.send(None)
except StopIteration as e:
print(f"{e.value = }")

上面的程序的输出结果如下所示:

name = 'bar'
e.value = 'foo'

上面两个协程都正确的执行完了代码,我们现在来看一下协程程序的字节码是怎么样的,上面的 foo 函数对应的字节码如下所示:

  9           0 LOAD_GLOBAL              0 (bar)
2 CALL_FUNCTION 0
4 GET_AWAITABLE
6 LOAD_CONST 0 (None)
8 YIELD_FROM
10 STORE_FAST 0 (name) 10 12 LOAD_GLOBAL 1 (print)
14 LOAD_CONST 1 ('name = ')
16 LOAD_FAST 0 (name)
18 FORMAT_VALUE 2 (repr)
20 BUILD_STRING 2
22 CALL_FUNCTION 1
24 POP_TOP 11 26 LOAD_CONST 2 ('foo')
28 RETURN_VALUE

在上面的代码当中和 await 语句相关的字节码有两条,分别是 GET_AWAITABLE 和 YIELD_FROM,在函数 foo 当中首先会调用函数 bar 得到一个协程对象,得到的这个协程对象会放到虚拟机的栈顶,然后执行 GET_AWAITABLE 这条字节码来说对于协程来说相当于没执行。他具体的操作为弹出栈顶元素,如果栈顶元素是一个协程对象,则直接将这个协程对象再压回栈顶,如果不是则调用对象的 __await__ 方法,将这个方法的返回值压入栈顶。

然后需要运行的字节码就是 YIELD_FROM,这个字节码和 "yield from" 语句对应的字节码是一样的,这就是为什么说协程就是生成器(准确的来说还是有点不一样,因为协程只是通过生成器的机制来完成,具体的实现需要编译器、虚拟机和标准库协同工作,才能够很好的完成协程程序,而且在虚拟机当中与协程有关的对象有好几个[都是基于生成器])。如果你不了解 YIELD_FROM 的工作原理,可以参考这篇文章:深入理解 Python 虚拟机:生成器停止背后的魔法

我们在使用生成器的方式来重写上面的程序:

def bar():
yield # 这条语句的主要作用是将函数编程生成器
return "bar" def foo():
name = yield from bar()
print(f"{name = }")
return "foo" if __name__ == '__main__':
generator = foo()
try:
generator.send(None) # 运行到第一条 yield 语句
generator.send(None) # 从 yield 语句运行完成
except StopIteration as e:
print(f"{e.value = }")

我们再来看一下 foo 函数的字节码:

  7           0 LOAD_GLOBAL              0 (bar)
2 CALL_FUNCTION 0
4 GET_YIELD_FROM_ITER
6 LOAD_CONST 0 (None)
8 YIELD_FROM
10 STORE_FAST 0 (name) 8 12 LOAD_GLOBAL 1 (print)
14 LOAD_CONST 1 ('name = ')
16 LOAD_FAST 0 (name)
18 FORMAT_VALUE 2 (repr)
20 BUILD_STRING 2
22 CALL_FUNCTION 1
24 POP_TOP 9 26 LOAD_CONST 2 ('foo')
28 RETURN_VALUE

字节码 GET_YIELD_FROM_ITER 就是从一个对象当中获取一个生成器。这个字节码会弹出栈顶对象,如果对象是一个生成器则直接返回,并且将它再压入栈顶,如果不是则调用对象的 __iter__ 方法,将这个返回对象压入栈顶。后续执行 YIELD_FROM 方法,就和前面的协程一样了。

总结

在本篇文章当中简单的介绍了一下协程是什么以及在 CPython 当中协程是通过什么方式实现的,从字节码的角度来看, 生成器和协程本质上使用的字节码是一样的,都是使用 YIELD_FROM 字节码实现的,协程就是在生成器的基础之上实现的。


本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

深入理解 Python 虚拟机:协程初探——不过是生成器而已的更多相关文章

  1. 深入理解Python中协程的应用机制: 使用纯Python来实现一个操作系统吧!!

    本文参考:http://www.dabeaz.com/coroutines/   作者:David Beazley 缘起: 本人最近在学习python的协程.偶然发现了David Beazley的co ...

  2. 深入理解 python 虚拟机:字节码灵魂——Code obejct

    深入理解 python 虚拟机:字节码灵魂--Code obejct 在本篇文章当中主要给大家深入介绍在 cpython 当中非常重要的一个数据结构 code object! 在上一篇文章 深入理解 ...

  3. 关于Python的协程问题总结

    协程其实就是可以由程序自主控制的线程 在python里主要由yield 和yield from 控制,可以通过生成者消费者例子来理解协程 利用yield from 向生成器(协程)传送数据# 传统的生 ...

  4. {python之协程}一 引子 二 协程介绍 三 Greenlet 四 Gevent介绍 五 Gevent之同步与异步 六 Gevent之应用举例一 七 Gevent之应用举例二

    python之协程 阅读目录 一 引子 二 协程介绍 三 Greenlet 四 Gevent介绍 五 Gevent之同步与异步 六 Gevent之应用举例一 七 Gevent之应用举例二 一 引子 本 ...

  5. 【Python】协程

    协程,又称微线程,纤程.英文名Coroutine. 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用. 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在 ...

  6. python的协程和_IO操作

    协程Coroutine: 协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行. 注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点 ...

  7. 深入理解 python 虚拟机:pyc 文件结构

    深入理解 python 虚拟机:pyc 文件结构 在本篇文章当中主要给大家介绍一下 .py 文件在被编译之后对应的 pyc 文件结构,pyc 文件当中的一个核心内容就是 python 字节码. pyc ...

  8. python gevent 协程

    简介 没有切换开销.因为子程序切换不是线程切换,而是由程序自身控制,没有线程切换的开销,因此执行效率高, 不需要锁机制.因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断 ...

  9. Python之协程(coroutine)

    Python之协程(coroutine) 标签(空格分隔): Python进阶 coroutine和generator的区别 generator是数据的产生者.即它pull data 通过 itera ...

  10. python 3 协程函数

    python 3 协程函数 1:把函数的执行结果封装好__iter__和__next__,即得到一个迭代器 2:与return功能类似,都可以返回值,但不同的是,return只能返回一次值,而yiel ...

随机推荐

  1. 前端Vue自定义带历史记录的搜索框组件searchBar 支持搜索输入框清空 搜索历史存储记录清除

    前端Vue自定义带历史记录的搜索框组件searchBar 支持搜索输入框清空 搜索历史存储记录清除,下载完整代码请访问uni-app插件市场地址:https://ext.dcloud.net.cn/p ...

  2. 前端Vue组件之仿京东拼多多领取优惠券弹出框popup 可用于电商商品详情领券场景使用

    随着技术的发展,开发的复杂度也越来越高,传统开发方式将一个系统做成了整块应用,经常出现的情况就是一个小小的改动或者一个小功能的增加可能会引起整体逻辑的修改,造成牵一发而动全身.通过组件化开发,可以有效 ...

  3. Prometheus-3:一文详解promQL

    读前提示: 本文字数较多且紧凑,最好预留15min一次性看完,好营养,易吸收. promQL详解 Prometheus提供了内置的数据查询语言PromQL(全称为Prometheus Query La ...

  4. Redis数据类型之Stream系列一

    一:Stream简介 ​ Redis Stream是5.0版本之后新增的一种数据结构,其结构类似于'仅追加日志'.但也实现了多种操作来克服'仅追加日志'的一些限制,如读取策略(xread,xrange ...

  5. [Spring+SpringMVC+Mybatis]框架学习笔记(八):Mybatis概述

    第8章 Mybatis概述 8.1 几个概念 ORM Object-Relationship Mapping 对象关系映射,它是一种思想,它的实质是将数据库中的数据用对象的形式表现出来. JPA Ja ...

  6. debezium同步postgresql数据至kafka

    0 实验环境 全部部署于本地虚拟机 debezium docker部署 postgresql.kafka本机部署 1 postgresql 1.1 配置 设置postgres密码为123 仿照exam ...

  7. EaselJS 源码分析系列--第四篇

    鼠标交互事件 前几篇关注的是如何渲染,那么鼠标交互如何实现呢? Canvas context 本身没有像浏览器 DOM 一样的交互事件 EaselJS 如何在 canvas 内实现自己的鼠标事件系统? ...

  8. uni-app学习之路

    MVC模式1. model:模型层,数据的增删改查2. view:视图层,前端页面3. controller:控制层,处理业务 文件页面结构1. 页面以`.vue`结尾2. `template`,`s ...

  9. Topic太多,RocketMQ炸了!

    网上博客常说,kafka的topic数量过多会影响kafka,而RocketMQ不会受到topic数量影响. 但是,果真如此吗? 最近排查一个问题,发现RocketMQ稳定性同样受到topic数量影响 ...

  10. nodejs中如何使用http创建一个服务

    http模块是nodejs中非常重要的一部分,用于开启一个服务,我们可以用它自定义接口供客户端使用.   开启服务的方式也比较简单,几行代码就可以搞定 const http = require('ht ...