深入理解 Python 虚拟机:协程初探——不过是生成器而已
深入理解 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 虚拟机:协程初探——不过是生成器而已的更多相关文章
- 深入理解Python中协程的应用机制: 使用纯Python来实现一个操作系统吧!!
本文参考:http://www.dabeaz.com/coroutines/ 作者:David Beazley 缘起: 本人最近在学习python的协程.偶然发现了David Beazley的co ...
- 深入理解 python 虚拟机:字节码灵魂——Code obejct
深入理解 python 虚拟机:字节码灵魂--Code obejct 在本篇文章当中主要给大家深入介绍在 cpython 当中非常重要的一个数据结构 code object! 在上一篇文章 深入理解 ...
- 关于Python的协程问题总结
协程其实就是可以由程序自主控制的线程 在python里主要由yield 和yield from 控制,可以通过生成者消费者例子来理解协程 利用yield from 向生成器(协程)传送数据# 传统的生 ...
- {python之协程}一 引子 二 协程介绍 三 Greenlet 四 Gevent介绍 五 Gevent之同步与异步 六 Gevent之应用举例一 七 Gevent之应用举例二
python之协程 阅读目录 一 引子 二 协程介绍 三 Greenlet 四 Gevent介绍 五 Gevent之同步与异步 六 Gevent之应用举例一 七 Gevent之应用举例二 一 引子 本 ...
- 【Python】协程
协程,又称微线程,纤程.英文名Coroutine. 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用. 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在 ...
- python的协程和_IO操作
协程Coroutine: 协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行. 注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点 ...
- 深入理解 python 虚拟机:pyc 文件结构
深入理解 python 虚拟机:pyc 文件结构 在本篇文章当中主要给大家介绍一下 .py 文件在被编译之后对应的 pyc 文件结构,pyc 文件当中的一个核心内容就是 python 字节码. pyc ...
- python gevent 协程
简介 没有切换开销.因为子程序切换不是线程切换,而是由程序自身控制,没有线程切换的开销,因此执行效率高, 不需要锁机制.因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断 ...
- Python之协程(coroutine)
Python之协程(coroutine) 标签(空格分隔): Python进阶 coroutine和generator的区别 generator是数据的产生者.即它pull data 通过 itera ...
- python 3 协程函数
python 3 协程函数 1:把函数的执行结果封装好__iter__和__next__,即得到一个迭代器 2:与return功能类似,都可以返回值,但不同的是,return只能返回一次值,而yiel ...
随机推荐
- 在js中修改less文件内样式
在项目中使用使用进度条时遇到了一点问题,根据需求进度条的百分比需要在条内显示,但是当完成度太低时由于进度条背景和百分比值都是接近的颜色,所以此时无法显示进度值,这个时候需要根据完成度大小来进行判断,动 ...
- 【IDEA】 远程调试
远程调试 使用特定JVM参数运行服务端代码 要让远程服务器运行的代码支持远程调试,则启动的时候必须加上特定的JVM参数,这些参数是: -Xdebug -Xrunjdwp:transport=dt_so ...
- 如何用Three.js + Blender打造一个web 3D展览馆
作者:vivo 互联网前端团队- Wei Xing 运营活动新玩法层出不穷,web 3D炙手可热,本文将一步步带大家了解如何利用Three.js和Blender来打造一个沉浸式web 3D展览馆. 一 ...
- Day09_Java_作业
A:简答题 1.什么是多态,多态的前提是什么? 2.多态中成员(成员变量,成员方法,静态成员方法)的访问特点是什么? 3.多态的好处? 4.多态的弊端是什么,如果我们想访问子类的特有的功能我们应该怎么 ...
- Hexo博客Next主题站内搜索模块相关,解决搜索无效、一直loading的问题
站内搜索配置 设置方法: 首先安装hexo-generator-searchdb插件 npm install hexo-generator-searchdb --save 编辑博客根目录下的博客本地目 ...
- 2.融合进阶:Stacking与Blending
1 堆叠法Stacking 1.1 堆叠法的基本思想 堆叠法Stacking是近年来模型融合领域最为热门的方法,它不仅是竞赛冠军队最常采用的融合方法之一,也是工业中实际落地人工智能时会考虑的方案之一. ...
- 关于 yield 关键字(C#)
〇.前言 yield 关键字的用途是把指令推迟到程序实际需要的时候再执行,这个特性允许我们更细致地控制集合每个元素产生的时机. 对于一些大型集合,加载起来比较耗时,此时最好是先返回一个来让系统持续展示 ...
- Linux 函数: my_func
# A man and his 'fuctions' ;) # quick use ipmitool cmd to do something ipmi-ip-cmd () { local ip=$1 ...
- 抽象类 vs 接口【概念解析系列_2】【C# 基础】
〇.前言 抽象类和接口的相似之处还是很多的,但是它们的侧重点不同,本文将简单梳理下. 一.简介与示例 1.1 抽象类 抽象类就是不能使用 new 方法进行实例化的类,即没有具体实例对象的类. 抽象类有 ...
- WPF中非递归(无后台代码)动态实现TreeView
在UI界面中,树形视图是比较常用的表示层级结构的方式,WPF中提供了TreeView控件.对于TreeView控件的基本使用已经有很多文章.大都是介绍如何在XAML中使用硬编码的固定信息填充Treev ...