深入了解 Python 生成器与协程机制

背景介绍

在 Python 中由于 GIL 锁的存在,多线程的并发效率不高。为了比较高效地实现并发,在 Python 中一般的方案是采用多进程 + 协程的方案。

协程也被称为纤线程,是一种程序级别的并发控制,多个协程会执行在同一线程中。协程的思想是由程序自身指定中断点,在 IO 操作时,程序可以自行中断,主动放弃 CPU,此时调度另外的协程继续运行。当 IO 就绪后,再调度此程序从中断点继续向下执行。

Python 中的协程是基于生成器实现的,在 Python 3 的版本演化中,生成器与协程的概念反复纠缠。导致这两个概念比较混杂,在这边梳理一下,也希望帮助后面的人少走弯路。

生成器 Generator

在 Python 最早的设计中,生成器是内部包含 yield 语句的函数。使用生成器可以比较优雅地实现一个迭代器。关于优雅迭代器的部分可以参考之前的 一篇博客 。为了更好地梳理整个流程,我们还是简要地回顾一下:

yield

yield 是 Python 2 中就开始提供的一个语句,可以程序中设置中断点,并在中断点返回特定的值,之后可以通过 next() 方法从中断点恢复程序的执行,运行到下一个中断点或程序执行结束。通过返回调用 next() 方法,每次获取一个值,即可实现类似迭代器的功能。

而内部包含 yield 的函数就被称为生成器,生成器的主要价值是实现优雅的迭代器。一个简单的代码如下所示:

def generator_func():yield 1yield 2gf = generator_func()
# 1print(next(gf))     
# 2print(next(gf))     
send()

理论上,生成器如果被用于实现迭代器的功能,借助 yield 就已经足够了,但是为了更好地支持协程的功能,在 pep-0324 中提到需要增强生成器,支持 send() 方法。通过 send() 方法我们不仅可以从生成器中获取值,还能给生成器传递值。此时我们不用关心为什么协程需要此方法,只是了解到我们现在得到一个更强大的生成器。

def generator_func():val1 = yield 1# 11print(f'val1: {val1}')           yield val1 + 2gf = generator_func()
first_val = next(gf)
# 1print(f'first val: {first_val}')      second_val = gf.send(first_val + 10)
# 13print(f'second val: {second_val}')    

通过上面的代码可以看到,我们不仅可以通过 next() 从生成器中不断获取新的值,还能通过 send() 给生成器传递值,生成器可以根据实际得到值生成新的值。从而支持更灵活和复杂的场景。

yield from

为了更好地支持协程,生成器被增强,在 pep-380 中支持了子生成器(Subgenerator)。这样可以引用其他生成器的,从而写出更加灵活的生成器。类似代码如下所示:

def generator_func():yield 1yield 2def new_generator_func():yield from generator_func()ngf = new_generator_func()
# 1next(ngf)          
# 2next(ngf)          

通过上面的代码,new_generator_func()generator_func() 函数一样,都是生成器。实现的效果是一样的。在实际中我们可以根据利用不同的生成器组合出更加复杂的生成器。

生成器总结

在上面的生成器的使用与增强中,可以理清生成器的概念。生成器本质是一个生成数据的机制。借助 yield 提供的中断恢复机制,可以不需要一次性生成所需的全部数据,可以不断执行,不断生产新的数据。

在生成器的增强中,虽然对基础的生成器而言不是必须的。但是也并不是加上了 send() 机制或 yield from 之后就变成了协程了,而是一种增强版本的生成器。网上不少博客在描述之中对在这个概念上的介绍中完全是胡说八道。

协程 Coroutine

协程是一种在程序级别的并发控制,多个协程会在同一线程中执行。由于同一时间一个线程中只有一个协程在执行,因此可以避免加锁的问题。基于最基础的生成器就可以实现协程。当然基于增强型的生成器可以实现更加有实用机制的协程。

下面以一个简单的生产者和消费者协程为例进行介绍:

def consumer():while True:d = yield 'data from consumer'print('[Consumer] get data from producer: %d' % d)def producer(consumer_obj):next(consumer_obj)for count in range(5):print('[Producer] producing %d' % count)consumer_obj.send(count)consumer_obj.close()c = consumer()
producer(c)

在上面的代码中,利用生成器实现的协程。可以在无锁的情况下实现从 producer()consumer() 传递数据。单独看 consumer() 还是生成器类型,但是整体是借助生成器实现任务的协作控制。

@asyncio.coroutine与yield from

在前面的例子表现,多个协程的协作还比较麻烦,看起来也还和原始的生成器类似,只能通过 send() 进行数据传递与任务恢复。在 pep-3156 开始引入事件循环,并可以通过 @asyncio.coroutine 显示地声明为协程,可以将多个协程在事件循环中运行。

一个简单的例子如下所示:

@asyncio.coroutine
def task1():while True:print('[Task1] Run task1 at %s' % datetime.now())yield from asyncio.sleep(2)@asyncio.coroutine
def task2():while True:print('[Task2] Run task2 at %s' % datetime.now())yield from asyncio.sleep(2)loop = asyncio.get_event_loop()
tasks = [task1(), task2()]
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()

在上面的任务中 asyncio.sleep(2) 模拟异步 IO 操作,可以看到在同一线程下,协程 task1()task2() 在事件循环中执行,当其中一个协程阻塞在模拟的 IO 操作时,就会放弃 CPU,另一个协程就会被切换执行。利用事件循环可以更充分地利用 CPU 资源,同时可以避免线程切换带来的开销。

async 和 await

在上面的代码中,虽然实现了协程的并行执行,但是如果去查看 task1() 的类型,会发现依旧是生成器类型(class generator),看起来还是类似生成器的应用。而且使用 @asyncio.coroutine 以及 yield from 看起来也并不优雅。

在 pep-0492 中 Python 提出了全新的 async 和 await 语法,对 @asyncio.coroutine 以及 yield from 进行了替换,按照新的语法改写上面的代码如下所示:

async def task1():while True:print('[Task1] Run task1 at %s' % datetime.now())await asyncio.sleep(2)async def task2():while True:print('[Task2] Run task2 at %s' % datetime.now())await asyncio.sleep(2)loop = asyncio.get_event_loop()
tasks = [task1(), task2()]
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()

在使用了全新的语法之后,task1() 的类型才变成了协程类型(class coroutine ) ,而从语法上,也与生成器的 yield 语法彻底区分开来。从各个方面将协程与生成器进行了区分。

总结

这篇文章是在梳理 Python 3 的异步操作时看到了各个技术博主随意使用生成器与协程,看着特别奇怪,新手真的会被各种奇怪的说法绕晕了。初看这两者,在 Python 中由于都是基于 yield 提供的中断与恢复机制,所以看起来确实很相似。但是从使用角度讲,这两者就有很大区别了。生成器本质上一个数据生产器,而协程是一个程序级别的并发控制机制。

当然如果直接从 async 和 await 用起来,那么就不会将协程与生成器弄混了。还是早日拥抱新特性吧。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/14931.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

同余定理性质

在算法题中碰到的这个同余定理定理,记录一下性质

RabbitMQ01-liunx下安装及用户权限分配

ErLang安装 RabbitMQ是使用ELang语言编写,所以在Liunx下安装RabbitMQ时要先安装ErLong依赖。 安装步骤 下载:https://www.erlang.org/downloads安装依赖: yum -y install make gcc gcc-c kernel-devel m4 ncurses-devel openssl-devel unixOD…

机器学习之常用算法与数据处理

一、机器学习概念: 机器学习是一门多领域交叉学科,涉及概率论、统计学、计算机科学等多门学科。它的核心概念是通过算法让计算机从数据中学习,改善自身性能。机器学习专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识…

Android Low Storage机制之DeviceStorageMonitorService

一、Android 版本 Android 13 二、low storage简介(DeviceStorageMonitorService) 设备存储监视器服务是一个模块,主要用来: 1.监视设备存储(“/ data”)。 2.每60秒扫描一次免费存储空间(谷歌默认值) 3.当设备的存储空间不足…

亚信安慧AntDB:数字化转型的关键力量

在数字化浪潮的推动下,数据已成为推动经济发展的新动力。亚信安慧AntDB数据库凭借其卓越性能和灵活的应用能力,在满足我国IT系统与产业数据多样化需求的过程中发挥着重要作用。AntDB数据库承载着无限可能,随着国家数字化转型的不断深入&#…

【项目】教你手把手完成博客系统(三)显示用户信息 | 实现退出登录 | 实现发布博客

文章目录 教你手把手完成博客系统(三)7.实现显示用户信息1.约定前后端交互接口2.前端通过ajax发起请求3.服务器处理请求 8.实现退出登录1.约定前后端的接口2.前端发起请求3.服务器处理请求 9.实现发布博客1.约定前后端的交互接口2.前端构造请求3.服务器处…

回溯法的重要延展题目

留个坑!!! 1.332重新安排行程 332. 重新安排行程 - 力扣(LeetCode) 2.51 N皇后问题 51. N 皇后 - 力扣(LeetCode) 思路: 印象中,对不同角度进行扫描,从…

web自动化-JavaScript操作

做UI自动化的时候,有些操作无法直接通过selenium自带方法操 作成功,那么就需要借助前端js操作实现。 比如浏览器的滚动条这种不是html页面的内容,无法直接通过selenium 控制到。需要借助JavaScript控制。比如有些点击操作无法通过普通点击鼠…

齐护K210系列教程(三十)_多任务切换

多任务切换 1,任务1的设定2,任务2的设定3,主程序4, 课程资源联系我们 在开发项目时,我们常会用到AIstart的多个任务来切换应用,比如当我识别到某种卡片时,要切换到别的任务,这样就要…

链游中的代币(Token)或加密货币(Cryptocurrency)是如何产生和使用的?

在区块链游戏(链游)中,代币和加密货币不仅是游戏经济的核心,也是连接现实世界与虚拟游戏世界的桥梁。这些数字货币不仅赋予了游戏内资产的真实价值,还为玩家提供了全新的互动和交易方式。下面,我们将深入探…

CentOS-7安装教程

目录 安装 修改主机名 配置静态IP 镜像下载地址 https://mirrors.aliyun.com/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso VMware Workstation Pro下载 VMware Workstation Pro各版本下载(2024.5.5之后)(Windows与Linux安装包不限…

【okhttp】小问题记录合集

can’t create native thread 问题描述 OkHttpClient 每次使用都new创建,造成OOM,提示can’t create native thread… 问题分析 没有将OkHttpClient单例化. 每个client对象都有自己的线程池和连接池,如果为每个请求都创建一个client对象&a…

使用v-model完成数据的双向绑定

创作灵感 面试问道了,没答出来,呜呜呜~ v-model实现双向绑定的原理 首先我们要知道,v-model实现的双向绑定其实只是props与emit的简化版本。其中,props负责父组件向子组件传递值,emit负责子组件向父组件传递值。 在…

视频推拉流EasyDSS系统如何在清理缓存文件的同时不影响缓存读写?

视频推拉流EasyDSS视频直播点播平台可提供一站式的视频转码、点播、直播、视频推拉流、播放H.265视频等服务,搭配RTMP高清摄像头使用,可将无人机设备的实时流推送到平台上,实现无人机视频推流直播、巡检等应用。 有用户咨询,视频推…

Git 的安装和使用

一、Git 的下载和安装 目录 一、Git 的下载和安装 1. git 的下载 2. 安装 二、Git 的基本使用-操作本地仓库 1 初始化仓库 1)创建一个空目录 2)git init 2 把文件添加到版本库 1)创建文件 2)git add . 3)g…

在SpringBoot自定义指标并集成Prometheus和Grafana监控

前沿 写这篇文章的目的是发现自己整天埋头写业务代码但忽略了主动发现问题的能力,这里指的是监控和报警。结合工作中发现Prometheus和Grafana还是主流一些。本文介绍如何使用自定义指标,并使用Prometheus进行监控并报警,同时在 Grafana 进行…

重学java 40.多线程 — 死锁和线程状态

—— 24.5. 一、死锁 1.死锁介绍(锁嵌套就有可能产生死锁) 指的是两个或者两个以上的线程在执行的过程中由于竞争同步锁而产生的一种阻塞现象;如果没有外力的作用,他们将无法继续执行下去,这种情况称之为死锁 例: 两线程处于互相等待的状态&a…

上位机图像处理和嵌入式模块部署(mcu常见三种烧录方法)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing 163.com】 和单纯的windows上位机开发、嵌入式linux开发不一样,mcu的开发,是需要通过烧录器把编译好的镜像烧入到开发板里面的。这是很…

Spark RDD 操作实战

Spark RDD 基础 更多spark相关知识请查看官方接口文档 PySpark是Spark的PythonAPI,允许Python调用Spark编程模型。 配置spark环境 !apt-get install openjdk-8-jdk-headless -qq > /dev/null !wget -q www-us.apache.org/dist/spark/spark-2.4.8/spark-2.4.8…