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

python · 浏览次数 : 0

小编点评

**协程简介** 协程是 Python 中一种新的并发编程机制,它允许程序在执行其他任务时保持执行状态,直到它们完成时才返回结果。协程与生成器类似,但协程更轻量级,因为它不直接创建新的线程,而是通过生成器来实现并发。 **协程实现** 在 Python 中,协程通过生成器实现。生成器是一个无限循环的函数,它一直等待其他协程或生成器发送请求。当一个协程执行完其任务后,它将发送一个 StopIteration 异常,告诉生成器它已完成工作。生成器会处理这些 StopIteration 异常并继续等待下一个请求。 **协程和生成器的区别** | 特性 |协程 | 生成器 | |---|---|---| | 线程创建 | 不需要创建新的线程 | 需要创建新的线程 | | 延迟 | 较低 | 较高 | | 资源利用率 | 更低 | 更高 | | 使用场景 | 当需要执行多个并发任务时 | 当需要处理大量请求时 | **协程示例** ```python def worker(queue): # 执行任务 result = queue.get() return result # 创建一个空的队列 queue = queue.Queue() # 提交两个任务到队列 queue.put(1) queue.put(2) # 从队列中获取结果 result = queue.get() # 打印结果 print(result) ``` **结论** 协程是一种新的并发编程机制,它提供了高效和易于管理的方法来实现并发操作。虽然协程与生成器相似,但协程更轻量级,因为它不直接创建新的线程。协程在需要执行多个并发任务时非常有用,因为它可以提高应用程序的性能。

正文

深入理解 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 3.4 Python 引入了一个非常有用的特性——协程,在本篇文章当中我们将详细介绍一下 Python 协程的原理以及虚拟机具体的实现协程的方式。

深入理解Python虚拟机:super超级魔法的背后原理

super 是 Python 面向对象编程当中非常重要的一部分内容,在本篇文章当中详细介绍了 super 内部的工作原理和 CPython 内部部分源代码分析了 super 的具体实现。

深入理解 python 虚拟机:原来虚拟机是这么实现闭包的

在本篇文章当中主要从虚拟机层面讨论函数闭包是如何实现的,所谓闭包就是将函数和环境存储在一起的记录。这里有三个重点一个是函数,一个是环境(简单说来就是程序当中变量),最后一个需要将两者组合在一起所形成的东西,才叫做闭包。

深入理解 python 虚拟机:生成器停止背后的魔法

在本篇文章当中主要分析的生成器内部实现原理和相关的两个重要的字节码,分析了生成器能够停下来还能够恢复执行的原因,深入剖析的生成器的原理的各个细节。

深入理解Python多进程:从基础到实战

title: 深入理解Python多进程:从基础到实战 date: 2024/4/29 20:49:41 updated: 2024/4/29 20:49:41 categories: 后端开发 tags: 并发编程 多进程管理 错误处理 资源调度 性能优化 异步编程 Python并发库 引言 在P

深入理解Python协程:从基础到实战

title: 深入理解Python协程:从基础到实战 date: 2024/4/27 16:48:43 updated: 2024/4/27 16:48:43 categories: 后端开发 tags: 协程 异步IO 并发编程 Python aiohttp asyncio 网络爬虫 第1章:协程

< Python全景系列-6 > 掌握Python面向对象编程的关键:深度探索类与对象

Python全景系列的第六篇,本文将深入探讨Python语言中的核心概念:类(Class)和对象(Object)。我们将介绍这些基本概念,然后通过示例代码详细展示Python中的类和对象如何工作,包括定义、实例化和修改等操作。本文将帮助您更深入地理解Python中的面向对象编程(OOP),并从中提出一些不常见但很有用的技术观点。

深入理解正则表达式:从入门到精通

title: 深入理解正则表达式:从入门到精通 date: 2024/4/30 18:37:21 updated: 2024/4/30 18:37:21 tags: 正则 Python 文本分析 日志挖掘 数据清洗 模式匹配 工具推荐 第一章:正则表达式入门 介绍正则表达式的基本概念和语法 正则表达

深入Scikit-learn:掌握Python最强大的机器学习库

> 本篇博客详细介绍了Python机器学习库Scikit-learn的使用方法和主要特性。内容涵盖了如何安装和配置Scikit-learn,Scikit-learn的主要特性,如何进行数据预处理,如何使用监督学习和无监督学习算法,以及如何评估模型和进行参数调优。本文旨在帮助读者深入理解Scikit-

前馈神经网络解密:深入理解人工智能的基石

> 本文深入探讨了前馈神经网络(FNN)的核心原理、结构、训练方法和先进变体。通过Python和PyTorch的实战演示,揭示了FNN的多样化应用。 > 作者TechLead,拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,