协程gevent模块和猴子补丁

亡梦爱人 提交于 2020-01-09 10:35:22

一、协程定义

协程其实可以认为是比线程更小的执行单元。 为啥说他是一个执行单元,因为他自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。

 

二、协程切换和线程切换对比

线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

 

三、协程带来的问题

协程有一个问题,就是系统并不感知,所以操作系统不会帮你做切换。 那么谁来帮你做切换?让需要执行的协程更多的获得CPU时间才是问题的关键。
 
举个例子如下:
 
目前的协程框架一般都是设计成 1:N 模式。所谓 1:N 就是一个线程作为一个容器里面放置多个协程。 那么谁来适时的切换这些协程?答案是有协程自己主动让出CPU,也就是每个协程池里面有一个调度器, 这个调度器是被动调度的。意思就是他不会主动调度。而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到), 这个时候就可以由这个协程通知调度器,这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要CPU的协程。 切换这个协程的CPU上下文把CPU的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况,或者它调用主动让出CPU的API之类,触发下一次调度。

 

四、协程处理好处

在IO密集型的程序中由于IO操作远远慢于CPU的操作,所以往往需要CPU去等IO操作。 同步IO下系统需要切换线程,让操作系统可以在IO过程中执行其他的东西。 这样虽然代码是符合人类的思维习惯但是由于大量的线程切换带来了大量的性能的浪费,尤其是IO密集型的程序。

(不开启多协程情况下,程序是按顺序执行的,上面的不执行完是不会执行下面的代码。这样的话如果有IO输入操作,这个时候程序就会自动暂停等待用户输入,那么这个等待的时间就被浪费了)
 
所以人们发明了异步IO。就是当数据到达的时候触发我的回调。来减少线程切换带来性能损失。 但是这样的坏处也是很大的,主要的坏处就是操作被 “分片” 了,代码写的不是 “一气呵成” 这种。 而是每次来段数据就要判断 数据够不够处理哇,够处理就处理吧,不够处理就在等等吧。
 
但是协程可以很好解决这个问题。比如 把一个IO操作 写成一个协程。当触发IO操作的时候就自动让出CPU给其他协程。要知道协程的切换很轻的。 协程通过这种对异步IO的封装 既保留了性能也保证了代码的容易编写和可读性。在高IO密集型的程序下很好。但是高CPU密集型的程序下没啥好处。

但是这样的话程序运行的顺序就会乱,不会按照程序代码顺序执行。很好理解嘛

 

五、猴子补丁有什么用?

先看一个例子

 1 import gevent,time
 2 
 3 def printf1(num):
 4     for i in range(num):
 5         print (i,gevent.getcurrent())
 6         time.sleep(1)
 7 
 8 def printf2(num):
 9     for i in range(num):
10         print (i,gevent.getcurrent())
11         time.sleep(1)
12 
13 def printf3(num):
14     for i in range(num):
15         print (i,gevent.getcurrent())
16         time.sleep(1)
17 
18 g1 = gevent.spawn(printf1,3)
19 g2 = gevent.spawn(printf2,2)
20 g3 = gevent.spawn(printf3,5)
21 g1.join()
22 g2.join()
23 g3.join()
24 
25 '''输出结果
26 0 <Greenlet at 0x27a15691598: printf1(3)>
27 1 <Greenlet at 0x27a15691598: printf1(3)>
28 2 <Greenlet at 0x27a15691598: printf1(3)>
29 0 <Greenlet at 0x27a156917b8: printf2(2)>
30 1 <Greenlet at 0x27a156917b8: printf2(2)>
31 0 <Greenlet at 0x27a156916a8: printf3(5)>
32 1 <Greenlet at 0x27a156916a8: printf3(5)>
33 2 <Greenlet at 0x27a156916a8: printf3(5)>
34 3 <Greenlet at 0x27a156916a8: printf3(5)>
35 4 <Greenlet at 0x27a156916a8: printf3(5)>
36 '''

我们会发现,我们用了协程,还有IO操作,但是为啥还是按顺序来的。

 

再看一个例子

 1 import gevent,time
 2 
 3 def printf1(num):
 4     for i in range(num):
 5         print ("这是第",i,gevent.getcurrent())
 6         gevent.sleep(1)  #就只改了一下这里
 7 
 8 def printf2(num):
 9     for i in range(num):
10         print ("这是第",i,gevent.getcurrent())
11         gevent.sleep(1)  #就只改了一下这里
12 
13 def printf3(num):
14     for i in range(num):
15         print ("这是第",i,gevent.getcurrent())
16         gevent.sleep(1)  #就只改了一下这里
17 
18 g1 = gevent.spawn(printf1,3)
19 g2 = gevent.spawn(printf2,3)
20 g3 = gevent.spawn(printf3,3)
21 g1.join()
22 g2.join()
23 g3.join()
24 
25 '''输出结果
26 这是第 0 <Greenlet at 0x1c0746f1598: printf1(3)>
27 这是第 0 <Greenlet at 0x1c0746f17b8: printf2(3)>
28 这是第 0 <Greenlet at 0x1c0746f16a8: printf3(3)>
29 这是第 1 <Greenlet at 0x1c0746f1598: printf1(3)>
30 这是第 1 <Greenlet at 0x1c0746f17b8: printf2(3)>
31 这是第 1 <Greenlet at 0x1c0746f16a8: printf3(3)>
32 这是第 2 <Greenlet at 0x1c0746f1598: printf1(3)>
33 这是第 2 <Greenlet at 0x1c0746f17b8: printf2(3)>
34 这是第 2 <Greenlet at 0x1c0746f16a8: printf3(3)>
35 '''

发现这才是我们想要的情况。我们发现用time.sleep(1)不行,用gevent.sleep(1)可以。我们知道其实他们的作用都是让程序休眠1秒。为什么time.sleep(1)不行呢

 

再看一个例子

 1 import gevent
 2 
 3 from gevent import monkey
 4 monkey.patch_all()
 5 import time
 6 def printf1(num):
 7     for i in range(num):
 8         print ("这是第",i,gevent.getcurrent())
 9         time.sleep(1)  #就只改了一下这里
10 
11 def printf2(num):
12     for i in range(num):
13         print ("这是第",i,gevent.getcurrent())
14         time.sleep(1)  #就只改了一下这里
15 
16 def printf3(num):
17     for i in range(num):
18         print ("这是第",i,gevent.getcurrent())
19         time.sleep(1)  #就只改了一下这里
20 
21 g1 = gevent.spawn(printf1,3)
22 g2 = gevent.spawn(printf2,3)
23 g3 = gevent.spawn(printf3,3)
24 g1.join()
25 g2.join()
26 g3.join()
27 
28 '''输出结果
29 这是第 0 <Greenlet at 0x1c0746f1598: printf1(3)>
30 这是第 0 <Greenlet at 0x1c0746f17b8: printf2(3)>
31 这是第 0 <Greenlet at 0x1c0746f16a8: printf3(3)>
32 这是第 1 <Greenlet at 0x1c0746f1598: printf1(3)>
33 这是第 1 <Greenlet at 0x1c0746f17b8: printf2(3)>
34 这是第 1 <Greenlet at 0x1c0746f16a8: printf3(3)>
35 这是第 2 <Greenlet at 0x1c0746f1598: printf1(3)>
36 这是第 2 <Greenlet at 0x1c0746f17b8: printf2(3)>
37 这是第 2 <Greenlet at 0x1c0746f16a8: printf3(3)>
38 '''

我们发现这次用的还是time.sleep(1),但是却符合我们的心理预期了

 

# 协程任务之间的切换由程序代码(gevent)完成,只有遇到协程模块能识别到的IO操作的时候,程序才会进行协程切换,实现并发的效果

# 在这个patch_all后面的所有模块中,发生的阻塞都会有gevent的效果

 1 # gevent模块
 2 # 协程,在一个线程中来回的切换。这个切换过程不是操作系统做的,而是gevent做的
 3 # 在这个patch_all后面的所有模块中,发生的阻塞都会有gevent的效果
 4 # #from gevent import monkey;monkey.patch_all()
 5 # import time
 6 # import gevent
 7 #
 8 # def eat():
 9 #     print('eating start')
10 #     time.sleep(1)   # 发生IO阻塞,切换到play.因为有了mokey.patch_all所以这里等同于gevent.sleep(1),遇见IO就会切换
11 #     print('eating end')
12 #
13 # def play():
14 #     print('playing start')
15 #     time.sleep(1) # 发生IO阻塞,切换到eat
16 #     print('playing end')
17 #
18 # if __name__ == '__main__':
19 #     g1 = gevent.spawn(eat)
20 #     g2 = gevent.spawn(play)
21 #     g1.join()   # 阻塞等待协程执行结束
22 #     g2.join()   # 

 

参考博客:

https://blog.csdn.net/u014745194/article/details/71657575

https://blog.csdn.net/weixin_34310369/article/details/93753220

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!