python yield原理_python学习之通过yield实现协程的理解

学习python遇到瓶颈,遇到问题搜索相关文章时垃圾信息太多,复制粘贴胡说八道,艰难梳理出有价值答案,决定整理自己的知识体系。

如果有人无意看到这篇文章,也可能我下面写的都是错的。

为什么使用协程(coroutine)?

进程是操作系统分配资源的最小单元, 线程是操作系统调度执行的最小单元。一个应用程序至少包括1个进程,而1个进程包括1个或多个线程。

由于进程启动切换的开销比较大,使用多进程的时候会导致大量内存空间被消耗,而如果使用多线程编程,虽然同一进程下的线程切换的开销小,但由于Python中GIL锁的存在,某个线程想要执行必须先拿到GIL,并且在一个python进程中GIL只有一个。所以在python开发中无论有cpu多少核,同时只能执行一个线程。

GIL锁的释放逻辑是当前线程遇见IO操作或者ticks计数达到100时进行释放。python3后GIL改为使用计时器,当执行时间达到阈值释放GIL锁。那么如果程序是CPU密集型的代码,无论是GIL锁采用ticks计数还是计时器计时来释放都必然造成CPU相较计算任务的负载而言,更频繁的处在线程的切换中,所以python的多线程编程对IO密集型代码更加友好。如果是CPU密集型的任务,更应该采用多进程编程。

协程相对多线程而言则只使用一个线程,将一个线程分解成为多个“微线程”,在一个线程中规定某个代码块的执行顺序,按逻辑来回切换执行代码块,通过yield人为的实现并发处理。即完成了任务也没有多线程切换的耗时,适用于有大量IO操作的代码。

python实现协程主要通过生成器的yield逻辑,或者在生产环境中一般采用第三方模块greenlet和gevent,功能强大,更易于理解。

那么对于不易于理解的yield实现协程的方法。

首先理解generator内的魔法方法:生成器可以:

通过yield 暂停执行和向外返回数据。Python的yield不但可以返回一个值,它还可以接收调用者通过send发出的参数

外部通过send()向生成器内发送数据

外部通过throw()向生成器内抛出异常以便随时终止生成器的运行

1式:n = yield r :

yield暂停本函数的执行,接收c.send(n)传来的值 n 赋值给等号左边 同时把r的值返回给调用者send()/next()

2式:r = c.send(n):

c.send(n)传值给yield,参数是n,并调用yield ,得到1式返回的值r 赋值给等号左边。

其实是互相传值

具体以廖大的“生产者消费者问题”代码为例:

def consumer(): #1

r = '' #2

while True: #3

n = yield r #4

if not n: #5

return #6

print('[CONSUMER] Consuming%s...' % n) #7

r = '200 OK' #8

#9

#10

def produce(c): #11

c.send(None) #12

n = 0 #13

while n < 5: #14

n = n + 1 #15

print('[PRODUCER] Producing%s...' % n) #16

r = c.send(n) #17

print('[PRODUCER] Consumer return:%s' % r)#18

c.close() #19

c = consumer() #22

produce(c) #23

执行结果:

[PRODUCER] Producing 1...

[CONSUMER] Consuming 1...

[PRODUCER] Consumer return: 200 OK

[PRODUCER] Producing 2...

[CONSUMER] Consuming 2...

[PRODUCER] Consumer return: 200 OK

[PRODUCER] Producing 3...

[CONSUMER] Consuming 3...

[PRODUCER] Consumer return: 200 OK

[PRODUCER] Producing 4...

[CONSUMER] Consuming 4...

[PRODUCER] Consumer return: 200 OK

[PRODUCER] Producing 5...

[CONSUMER] Consuming 5...

[PRODUCER] Consumer return: 200 OK

理解代码:line 22-23:创建生成器c,作为参数传入produce函数运行

line 12:c.send(None) 调用yield,向生成器c内发送数据,也就是参数None

line 4-6:跳转到consumer函数;yield得到发送来的值None 赋值给n,n为None;返回r =' ' 给调用者“c.send(None)” ;中断此次consumer函数;

line 13 - 17:跳转到produce函数; n = 1;打印“Producing 1...” ;r = c.send(n) 发送n = 1 给生成器c;

“注意 send和next不同之处在于:send函数带有一个参数,这个参数会覆盖consumer里上一个yield语句收到的n的值 就是第四行n这里不再是None而是1”跳转到consumer函数,从第五行开始执行,此时n = 1,经过第五行if判断,打印“Consuming 1...”; r = '200 OK';回到循环体开头,执行第四行yield语句,返回r = '200 OK' n= yield r n值没有改变 仍然是1;中断此次consumer函数;第17行r值接收返回值变为'200 OK';

line 18:跳转到produce函数;打印 Consumer return: 200 OK回到循环体头部

line 15 - 17: n = 2,打印 Producing 2... 执行r = c.send(n) 发送n = 2 到生成器c

line 5:跳转到consumer函数,从上一个yield的下一条代码执行,n值被line 17 send函数发送改写成2;打印 Consuming 2...;r = '200 OK';回到循环体开头,执行第四行yield语句,返回r值给line 17的send函数,line 4的 n值不变,仍然是2;中断此次consumer函数

line 17:跳转到produce函数;打印 Consumer return: 200 OK;

重复上述循环······

至于在web编程中利用 gevent 配合 wsgi 服务器如 gunicorn 提高并发性能,可以通过 gevent 库配置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值