python协程系列(二)——python协程的通俗理解以及使用yield关键字实现协程
查看:https://blog.csdn.net/qq_27825451/article/details/85234610
一,什么是协程(coroutine)
1,协程定义
协程,又称微线程,纤程。英文名Coroutine。协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。
2,子程序,或者称为函数
在所有语言中都是层级调用,比如A调用B,B在执行的过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一个返回,调用的顺序是明确的。
顺序执行的缺点:这里小伙伴们都应该是分清楚,那就是程序无休此等待,必须等待一个函数执行完之后才返回结果。
3,多线程
避免顺序执行的方式之一是多线程,但是考虑到python语言的特性(GIL锁),再执行计算密集型的任务时,多线程的执行效果反而变慢,再执行IO密集型的任务时候虽然有不错的性能提升,但是依然会有线程管理与切换、同步的开销等等(具体原因这里不详细说明,请参见相关的GIL说明)
4,协程
协程有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
优势一:最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是又程序自身控制,因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
优势二:就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
5,多进程+协程
因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
6,协程于一般函数的不同点
协程看上去也是子程序(函数),但执行过程中,在子程序内部(函数)可中断,而不是一次性一定要执行完才行,然后转而执行别的子程序,在适当的时候再返回来接着执行。
二,协程(coroutine的直观理解)
1,协程的直观理解
yield个人认为其实是为了实现协程而出现的。所以如果要解释清楚什么是yield,那么也就必须要先搞懂什么是协程。首先明确一点:协程是针对单个CPU的,也就是说,讲协程讲的就是单线程。我们可以通过协程实现类似并发的任务,并且如果只是在一个CPU上的话,使用协程带来的效率一般都会比使用线程来的高。这是为啥呢?这就要看协程的原理了。
协程的原理很简单,打个比方就能讲明白了:假设说有十个人去食堂打饭,这个食堂比较穷,只有一个打饭的窗口,并且也只有一个打饭阿姨,那么打饭就只能一个一个排队来打咯。这十个人胃口很大,每个人都要点5个菜,但这十个人又有个毛病就是做事情都犹豫不决,所以点菜的时候就会站在那里,每点一个菜后都会想下一个菜点啥,因此后面的人等的很着急呀。这样一直站着也不是个事情吧,所以打菜的阿姨看到某个人犹豫5秒后就开始吼一声,会让他排到队伍最后去,先让别人打菜,等轮到他的时候他也差不多想好吃啥了。这确实是个不错的方法,但也有一个缺点,那就是打菜的阿姨会等每个人5秒钟,如果那个人在5秒内没有做出决定吃啥,其实这5秒就是浪费了。一个人点一个菜就是浪费5秒,十个人每个人点5个菜可就浪费的多啦(菜都凉了要)。那咋办呢?
这个时候阿姨发话了:大家都是学生,学生就要自觉,我以后也不主动让你们排到最后去了,如果你们觉得自己会犹豫不决,就自己主动点直接点一个菜就站后面去,等下次排到的时候也差不多想好吃啥了。这个方法果然有效,大家点了菜后想的第一件事情不是下一个菜吃啥,而是自己会不会犹豫,如果会犹豫那直接排到队伍后面去,如果不会的话就直接接着点菜就行了。这样一来整个队伍没有任何时间是浪费的,效率自然就高了。
这个例子里的排队阿姨的那声吼就是我们的CPU中断,用于切换上下文。每个打饭的学生就是一个task。而每个人自己决定自己要不要让出窗口的这种行为,其实就是我们协程的核心思想。
在用线程的时候,其实虽然CPU把时间给了你,你也不一定有活干,比如你要等IO、等信号啥的,这些时间CPU给了你你也没用呀。
在用协程的时候,CPU就不来分配时间了,时间由你们自己决定,你觉得干这件事情很耗时,要等IO啥的,你就干一会歇一会,等到等IO的时候就主动让出CPU,让别人上去干活,别人也是讲道理的,干一会也会把时间让给你。协程就是使用了这种思想,让编程者控制各个任务的运行顺序,从而最大可能的发挥CPU的性能。
2,为什么yield可以实现协程
在Python中,协程通过yield实现。因为当一个函数中又yield存在的时候,这个函数是生成器,那么当你调用这个函数的时候,你在函数体重写的代码并没有被执行,而是返回了一个生成器对象,这个需要特别注意。然后,你的代码会在每次使用这个生成器的时候被执行。
前面讲过yiedl表达式的两个关键作用:①返回一个值②接收调用者参数
“调用者”与“被调用者”之间的通信是通过send()进行联系的
正是因为yield实现的生成器具备“中断等待的功能”,才使得yield可以实现协程。
3,yield实现协程的例子
(1)实例一:
生产者-消费者模型。这里不讨论生产者-消费者模式到底有什么用,这里要实现的就是简单的函数调用。代码如下:
生产者消费者.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | def consumer(): # 定义返回 r = '' while True : # 执行的中断点 n = yield r # 如果n为空则return生成器抛出StopIteration异常 # 实际本例n一直不为空这一段永不执行 if not n: return # 消费者消费通过生产者send(value)过来的参数 print ( '[消费者]正在消费:{0}' . format (n)) # 设置返回值r r = '200 人民币' def producer(c): # 启动消费者(生成器),实际上是函数调用,只不过生成器不是直接像函数那般调用 # 使用send()第一次启动生成器传递的参数只能是None否则报错 c.send( None ) n = 0 while n < 5 : n = n + 1 print ( '[生产者]正在生产:{0}' . format (n)) # 生产者往消费者发送消息 # 给消费者传入值,实际也是函数调用 r = c.send(n) print ( '[生产者]消费者返回:{0}' . format (r)) print ( '-------------------------------' ) # 关闭生成器 c.close() c = consumer() producer(c) |
运行输出如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | PS D:\learn - python3\函数式编程> & c: / python36 / python.exe d: / learn - python3 / 学习脚本 / 协程系列 / 生产者消费者.py [生产者]正在生产: 1 [消费者]正在消费: 1 [生产者]消费者返回: 200 人民币 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [生产者]正在生产: 2 [消费者]正在消费: 2 [生产者]消费者返回: 200 人民币 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [生产者]正在生产: 3 [消费者]正在消费: 3 [生产者]消费者返回: 200 人民币 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [生产者]正在生产: 4 [消费者]正在消费: 4 [生产者]消费者返回: 200 人民币 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [生产者]正在生产: 5 [消费者]正在消费: 5 [生产者]消费者返回: 200 人民币 |
解释分析:
第一步:在produece(c)函数中,调用c.send(None)启动了生成器,这相当于调用consumer(),但是如果consumer是一个普通函数而不是生成器,就要等到consumer执行完了,主动权才会回到produce手里。但就是因为consumer是生成器,所以第一次遇到yield暂停。接着执行produce()中接下来的代码,从运行结果看,确实打印出了[生产者]正在生产1,当程序运行至c.send(n)时,再次调用生产并且通过yiele传递了参数(n=1),这个时候进入consumer()函数先前在yield停下的地方,继续往后执行,所以打印出[消费者]正在消费1.
第二步:[消费者]正在消费1这句话被打印出来之后,接下来consumer()函数中此时r被赋值为'200 人民币',接着cousumer()函数里面的第一次循环结束,进入第二次循环,又遇到yield,所以consumer()函数又暂停并且返回变量r的值,consumer()函数暂停,此时程序有进入produce()函数中接着执行。
第三步:由于先前produce(c)函数接着第一次循环中c.send(n)处相当于调用消费者consumer(),跳入到了consumer()里面去执行,现在cousumer()暂停,prodecer重新掌握主动权,故而继续往下执行打印出[生产者]消费者返回:200 人民币,然后打印出分隔符之后,prodecer的第一次循环结束,并进行第二次信息,打印出[生产者]正在生产2,然后又调用c.send(n)又调用消费者consumer,将控制器交给consumer,如此循环...
(2)实例二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import time #定义一个消费者,他有名字name #因为里面有yield,本质上是一个生成器 def consumer(name): print (f '{name} 准备吃包子啦!,呼吁店小二' ) while True : baozi = yield #接收send传的值,并将值赋值给变量baozi print (f '包子 {baozi+1} 来了,被 {name} 吃了!' ) #定义一个生产者,生产包子的店家,店家有一个名字name,并且有两个顾客c1 c2 def producer(name,c1,c2): next (c1) #启动生成器c1 next (c2) #启动生成器c2 print (f '{name} 开始准备做包子啦!' ) for i in range ( 5 ): time.sleep( 1 ) print (f '做了第{i+1}包子,分成两半,你们一人一半' ) c1.send(i) c2.send(i) print ( '------------------------------------' ) c1 = consumer( '张三' ) #把函数变成一个生成器 c2 = consumer( '李四' ) producer( '店小二' ,c1,c2) |
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | D:\learn - python3\函数式编程> & c: / python36 / python.exe d: / learn - python3 / 学习脚本 / 协程系列 / 生产包子.py 张三 准备吃包子啦!,呼吁店小二 李四 准备吃包子啦!,呼吁店小二 店小二 开始准备做包子啦! 做了第 1 包子,分成两半,你们一人一半 包子 1 来了,被 张三 吃了! 包子 1 来了,被 李四 吃了! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 做了第 2 包子,分成两半,你们一人一半 包子 2 来了,被 张三 吃了! 包子 2 来了,被 李四 吃了! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 做了第 3 包子,分成两半,你们一人一半 包子 3 来了,被 张三 吃了! 包子 3 来了,被 李四 吃了! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 做了第 4 包子,分成两半,你们一人一半 包子 4 来了,被 张三 吃了! 包子 4 来了,被 李四 吃了! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 做了第 5 包子,分成两半,你们一人一半 包子 5 来了,被 张三 吃了! 包子 5 来了,被 李四 吃了! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
运行过程分析:
第一步:启动生成器c1,c2.c1先运行,运行到第一个循环的yield,暂停,然后c2运行,也运行到第一个yield暂停,打印得到
张三 准备吃包子啦!,呼吁店小二
李四 准备吃包子啦!,呼吁店小二
第二步:现在相当于两个顾客等着吃包子,控制权交给店小二生产包子,于是打印出 店小二 开始准备做包子啦!,并且进入producer的第一个循环,花了1秒钟,生产第一个包子,然后将其一分为二,打印出:做了第1包子,分成两半,你们一人一半。
第三步:此时producer店小二调用send()函数,相当于将包子给两位客人,这个时候先执行c1.send(),即先把包子给c1,然后c1获得了控制权,打印出包子 1 来了,被 张三 吃了!然后他吃完进入第二次循环遇见了yield,又暂停。控制权重新回到producer手上,他再执行c2.send(),将包子给c2,c2掌握控制权,于是打印出 包子 1 来了,被 李四 吃了!它在进入第二次循环,遇到yield,然后又暂停了,控制权重新回到producer店小二手中,店小二打印出一段虚线,然后进入第二次循环,重新花了1秒钟,又做了一个包子,一次这样下去。
(3)实例三
往生成器传递数字计算平均值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | def average(): # 数字的总和 total = 0.0 # 数字的个数 count = 0 # 数字的平均值 avg = None while True : # 平均值作为生成器的返回 n = yield avg # 没跌倒一次生成器数字传递的数字个数+1 count = count + 1 # 计算传递数字的和 total = total + n # 计算平均值 avg = total / count a = average() a.send( None ) # 定义一个函数,通过这个函数想average函数发送数值 def sender(generator): # 第一步启动生成器 next (generator) print (generator.send( 10 )) print (generator.send( 20 )) print (generator.send( 30 )) print (generator.send( 40 )) g = average() sender(g) # 10.0 # 15.0 # 20.0 # 25.0 |
运行步骤为
第一步:启动生成器,停留在yield
第二步:使用send(10)传递数字10进入生成器计算平均值为10,作为生成器yield的返回值,然后依次传递20,30,40,依次打印计算出来的平均值
三,协程的状态查看
我们知道,协程是可以暂停等待的,然后又恢复的生成器函数,那么我们有没有什么办法查看一个协程到低是处于什么状态呢?协程有四种状态,它们分别是:
GEN_CREATED:等待执行,即还没有进入协程
GEN_RUNNING:解释器执行(这个只有在使用多线程时才能查看到他的状态,而协程是单线程的)
GEN_SUSPENDED:在yield表达式处暂停(协程在暂停等待的时候的状态)
GEN_CLOSED:执行结束(协程执行结束了之后的状态)
怎么查看呢?
协程的状态可以用inspect.getgeneratorstate()函数来确定,实例如下:
协程的状态.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | from inspect import getgeneratorstate #一定要导入 from time import sleep def my_generator(): for i in range ( 3 ): sleep( 0.5 ) x = yield i + 1 g = my_generator() #创建一个生成器对象 def main(generator): try : print ( "生成器初始状态为:{0}" . format (getgeneratorstate(g))) next (g) #激活生成器 print ( "生成器初始状态为:{0}" . format (getgeneratorstate(g))) g.send( 100 ) print ( "生成器初始状态为:{0}" . format (getgeneratorstate(g))) next (g) print ( "生成器初始状态为:{0}" . format (getgeneratorstate(g))) next (g) except StopIteration: print ( '全部迭代完毕了' ) print ( "生成器初始状态为:{0}" . format (getgeneratorstate(g))) |
运行结果如下
1 2 3 4 5 6 7 | PS D:\learn - python3\函数式编程> & c: / python36 / python.exe d: / learn - python3 / 学习脚本 / 协程系列 / 协程的状态.py 生成器初始状态为:GEN_CREATED 生成器初始状态为:GEN_SUSPENDED 生成器初始状态为:GEN_SUSPENDED 生成器初始状态为:GEN_SUSPENDED 全部迭代完毕了 生成器初始状态为:GEN_CLOSED |
可以看到创建一个生成器没有初始化处于等待状态,一旦开始跌打就处于yield暂停等待状态,迭代完毕或者手动关闭处于关闭状态。
四,yield实现协程的不足之处
(1)协程函数的返回值不是特别方便获取,为什么参见上一篇文章,只能够通过捕获StopIteration异常,然后通过该异常的value属性获取。
(2)Python的生成器是协程corontine的一种形式,但它的局限性在于只能向它的直接调用者每次yield一个值。这意味着那些包含yield的代码不能像其他代码那样被分离出来放到一个单独的函数中。这也正是yield from要解决的。
全文总结
从某些角度来理解,协程其实就是一个可以暂停执行的函数,并且可以恢复继续执行。那么yield已经可以暂停执行了,如果在暂停后有办法把一些 value 发回到暂停执行的函数中,那么 Python 就有了『协程』。于是在PEP 342中,添加了 “把东西发送到已经暂停的生成器中” 的方法,这个方法就是send()。
下文预告:下一篇讲解yield from的详细使用方法
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
2020-10-12 Python基础
2019-10-12 Nginx日志挂载目录为nfs文件服务器时开机无法自启动的问题解决