python协程(1): 基本介绍及yield实现协程

您所在的位置:网站首页 Yield原理及工具介绍 python协程(1): 基本介绍及yield实现协程

python协程(1): 基本介绍及yield实现协程

2024-05-24 11:49| 来源: 网络整理| 查看: 265

一. 协程介绍

协程,又称微线程,纤程。英文名Coroutine。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

协程不同于线程的是,线程是抢占式的调度,而协程是协同式的调度,也就是说,协程需要自己做调度。

协程的好处:

无需线程上下文切换的开销

无需原子操作锁定及同步的开销

方便切换控制流,简化编程模型

高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

缺点:

无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。

进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

协程的演进

1.greenlet 早期模块

2.yield关键字

3.asyncio装饰器(python3.4加入)

4.async、await关键字(python3.5加入)推荐使用

二.yield实现协程

2.1 为什么yield可以实现协程

在Python中,协程通过yield实现。因为当一个函数中有yield存在的时候,这个函数是生成器,那么当你调用这个函数的时候,你在函数体中写的代码并没有被执行,而是只返回了一个生成器对象,这个需要特别注意。然后,你的代码将会在每次使用这个生成器的时候被执行。

前面讲过yield表达式的两个关键作用:①返回一个值、②接收调用者的参数

“调用者”与“被调用者”之间的通信是通过send()进行联系的

正是因为yield实现的生成器具备“中断等待的功能”,才使得yield可以实现协程。

例1:生产者消费者模型

def consumer(): r = '' while True: n = yield r #执行的中断点 if not n: return print('[消费者] 正在消费:{0}'.format(n)) r = '200 人民币' def produce(c): c.send(None) #启动消费者(生成器)——实际上是函数调用,只不过生成器不是直接象函数那般调用的 n = 0 while n < 3: n = n + 1 print('[生产者] 正在生产:{0}'.format(n)) r = c.send(n) #给消费者传入值——实际上也是函数调用 print('[生产者] 消费者返回:{0}'.format(r)) print('-------------------------------------------------') c.close() c = consumer()#构造一个生成器 produce(c) '''运行结果为: [生产者] 正在生产:1 [消费者] 正在消费:1 [生产者] 消费者返回:200 人民币 ------------------------------------------------- [生产者] 正在生产:2 [消费者] 正在消费:2 [生产者] 消费者返回:200 人民币 ------------------------------------------------- [生产者] 正在生产:3 [消费者] 正在消费:3 [生产者] 消费者返回:200 人民币 ------------------------------------------------- '''

解释分析:

第一步:在produce(c)函数中,调用了c.send(None)启动了生成器,这相当于是调用consumer(),但是如果consumer是一个普通函数而不是生成器,就要等到consumer执行完了,主动权才会重新回到producer手里。但就是因为consumer是生成器,所以第一次遇到yield暂停;接着执行produce()中接下来的代码,从运行结果看,确实打印出了[生产者] 正在生产 1 ,当程序运行至c.send(n)时,再次调用生成器并且通过yield传递了参数(n = 1),这个时候,进入consumer()函数先前在yield停下的地方,继续向后执行,所以打印出[消费者] 正在消费 1。

第二步:[消费者] 正在消费 1  这句话被打印出来之后,接下consumer()函数中此时 r 被赋值为’200 人民币’,接着consumer()函数里面的第一次循环结束,进入第二次循环,又遇到yield, 所以consumer()函数又暂停并且返回变量 r 的值,consumer()函数暂停,此时程序又进入produce(c)函数中接着执行。

第三步:由于先前produce(c)函数接着第一次循环中c.send(n)处相当于是调用消费者consumer(),跳入到了consumer()里面去执行,现在consumer暂停,producer重新我有主动权,故而继续往下执行打印出[生产者] 消费者返回: 200 人民币,然后producer的第一次循环结束,并进行第二次循环,打印出[生产者] 正在生产 1,然后,又调用c.send(n) 又调用消费者consumer,将控制权交给consumer,如此循环回到第一步!

例2:

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(3): time.sleep(1) print(f'做了第{i+1}包子,分成两半,你们一人一半') c1.send(i) c2.send(i) print('------------------------------------') c1=consumer('张三') #把函数变成一个生成器 c2=consumer('李四') producer('店小二',c1,c2) '''运行结果为: 张三 准备吃包子啦!,呼吁店小二 李四 准备吃包子啦!,呼吁店小二 店小二 开始准备做包子啦! 做了第1包子,分成两半,你们一人一半 包子 1 来了,被 张三 吃了! 包子 1 来了,被 李四 吃了! ------------------------------------ 做了第2包子,分成两半,你们一人一半 包子 2 来了,被 张三 吃了! 包子 2 来了,被 李四 吃了! ------------------------------------ 做了第3包子,分成两半,你们一人一半 包子 3 来了,被 张三 吃了! 包子 3 来了,被 李四 吃了! ------------------------------------ '''

运行过程分析:

第一步:启动生成器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:

def average(): total = 0.0 #数字的总和 count = 0 #数字的个数 avg = None #平均值 while True: num = yield avg total += num count += 1 avg = total/count #定义一个函数,通过这个函数向average函数发送数值 def sender(generator): print(next(generator)) #启动生成器 print(generator.send(10)) # 10 print(generator.send(20)) # 15 print(generator.send(30)) # 20 print(generator.send(40)) # 25 g = average() sender(g) '''运行结果为: None 10.0 15.0 20.0 25.0 '''

三、协程的状态查看

我们都知道,协程是可以暂停等待、然后又恢复的生成器函数,那么我又没有什么方法查看一个协程到底是处于什么状态呢?协程有四种状态,它们分别是:

协程有四种状态,分别是

GEN_CREATED:等待执行,即还没有进入协程

GEN_RUNNING:解释器执行(这个只有在使用多线程时才能查看到他的状态,而协程是单线程的)

GEN_SUSPENDED:在yield表达式处暂停(协程在暂停等待的时候的状态)

GEN_CLOSED:执行结束(协程执行结束了之后的状态)

协程的状态可以用inspect.getgeneratorstate()函数来确定,实例如下:

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))) main(g) '''运行结果为: 生成器初始状态为:GEN_CREATED 生成器初始状态为:GEN_SUSPENDED 生成器初始状态为:GEN_SUSPENDED 生成器初始状态为:GEN_SUSPENDED 全部迭代完毕了 生成器初始状态为:GEN_CLOSED ''' 四、yield实现协程的不足之处

(1)协程函数的返回值不是特别方便获取,参照 python 生成器(1)-- 基本使用,只能够通过出发StopIteration异常,然后通过该异常的value属性获取;

(2)Python的生成器是协程coroutine的一种形式,但它的局限性在于只能向它的直接调用者每次yield一个值。这意味着那些包含yield的代码不能想其他代码那样被分离出来放到一个单独的函数中。这也正是yield from要解决的。

全文总结:

从某些角度来理解,协程其实就是一个可以暂停执行的函数,并且可以恢复继续执行。那么yield已经可以暂停执行了,如果在暂停后有办法把一些 value 发回到暂停执行的函数中,那么 Python 就有了『协程』。于是在PEP 342中,添加了 “把东西发送到已经暂停的生成器中” 的方法,这个方法就是send()

参考:

python协程通俗理解

谈谈协程技术的演进

python之协程



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3