python2.0_s12_day9_协程&Gevent协程
Posted zhming
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python2.0_s12_day9_协程&Gevent协程相关的知识,希望对你有一定的参考价值。
Python之路,Day9 - 异步IO\数据库\队列\缓存
本节内容
Gevent协程
Select\Poll\Epoll异步IO与事件驱动
Python连接mysql数据库操作
协程
1.协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程。(操作系统跟不知道它存在),那你指定协程的实现原理是什么吗?
我们来聊聊协程的实现原理:
首先我们知道多个线程在一个单核CPU上进行并发,它的操作过程是,操作系统能调动的最小单位是线程,当操作系统触发多个线程到一个单核心的CPU上,接下来线程如何处理,怎样切换就不是操作系统能控制的了,那是由谁控制的,由硬件CPU,或者其他硬件控制的.它利用一个机制,将每一个线程切片,然后轮询将每一个线程的分片交给CPU处理.
但是你要知道,CPU同一时刻只能处理一个线程的分片.(那它是怎样将每一个分片对应到相应的线程的,就是通过CPU自己的寄存器,上下文,堆栈信息存储的.总之利用这些CPU会对应每一个线程的处理数据包.)也就是说,CPU也还是串行处理线程的任务的,只是它切换的特别快,让人类觉得是并行处理的.
那么这个协程有什么关系呢?当然有关系,协程正是python在代码中模仿单核CPU处理多线程的原理,利用代码造就了一个代码切换的机制,这个机制就是执行完有IO操作或者sleep这种操作的代码块后就切换到其他代码段,这样执行的时间就会缩短.因为串行中要等待处理后结果的操作时,这里用做执行其他代码了,等结果返回后在利用自有的寄存器\上下文\堆栈等信息对应到相应的代码段即可.
需要注意的是,单核CPU处理多线程时,硬件在不同线程间切换时要消耗时间.而协程技术所产生的切换动作是在一个线程中进行的.虽然协程的切换也要消耗时间,但是它不涉及到线程间的切换,只是CPU在处理一个线程代码时在代码段间不停的切换,所以协程的切换效率要比CPU单核处理多线程的效率还要高.消耗的时间还要短.
以上就是python协程实现的原理!
2.协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
协程的好处:
无需线程上下文切换的开销
无需原子操作锁定及同步的开销
方便切换控制流,简化编程模型
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
那么我们还有一个疑问,协程和多线程哪个速度更快\执行效率更高.
我们知道Cpython有一个GIL全局解释性锁的特性.那么有这个锁导致的结果是,我有两种猜想:
1.python启用多线程后,比如8个线程,也许打到了2CPU的8个核心上,但是GIL会导致这些线程在同一时刻只有一个线程在执行.
2. python启用多线程后,python解释器层控制这在代码上生成的多线程轮询调用C语言的线程,实际上这些多线程,最终只是轮询调用c语言的一个线程接口.
上面两种对GIL实际的操作的猜测,我偏向于2.因为如果按照1中打到CPU的8个核心上的8个线程,后面就不在受操作系统原生线程的控制,而是硬件调度CPU处理线程里的数据包.所以全局解释性锁根本就控制不了.
那么如果是情况2 ,就意味着,GIL其实起到的左右和协程一样.那他们的效率应该也差不多吧.但是GIL这个貌似被看作是CPython的诟病的特性,应该效率比协程要低一些.
所以结论是,一般如果能用协程方式的程序用协程.并且用了协程后就不要在用多线程,因为两个一样的东西,没意义.用线程反而能更好的利用多核的硬件优势.
具体想想,协程应用场景和线程的应用场景应该有所不同,举个例子,如果像我们之前的例子,对一个全局变量,调用多线程进行递减,会有锁的问题.但是这时候你可不能用协程的方式,调用同一个函数,对同一个变量进行更改.
如果你调用了,那同样还是有这么一个锁的问题存在,因为你在有IO操作后,切换到下一个对同一个变量进行递减的操作.这结果还不是乱套.所以我觉得协程应用场景应该是多个不同函数调用的时候使用,减少对有IO操作和sleep这样操作等待的时间.
使用yield实现协程操作例子
1 2 #!/usr/bin/env python3.5 3 #__author__:ted.zhou 4 ‘‘‘ 5 使用yield实现协程的例子 6 ‘‘‘ 7 import time 8 import queue 9 def consumer(name): 10 print("--->starting eating baozi...") 11 while True: 12 new_baozi = yield 13 print("[%s] is eating baozi %s" % (name,new_baozi)) 14 #time.sleep(1) 15 16 def producer(): 17 18 r = con.__next__() # python3.0里变成__next__(),python2.0是next() 19 r = con2.__next__() 20 n = 0 21 while n < 5: 22 time.sleep(1) # 模拟阻塞1秒 23 n +=1 24 con.send(n) 25 con2.send(n) 26 print("\033[32;1m[producer]\033[0m is making baozi %s" %n ) 27 28 29 if __name__ == ‘__main__‘: 30 con = consumer("c1") 31 con2 = consumer("c2") 32 p = producer()
我们看上面的例子,其实就是一个简单的协程首先,con,con2分别都执行了一部分代码,直到调用con.send(),con2.send()方法才继续后面的代码.
那么问题来了,如果生产者每做一个包子要花费1秒钟,那么相当于每次生产包子的时候产生1秒中的阻塞后,才能继续下面的代码,这就是前面提到的"进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序",影响到整个线程.
我们想实现,一旦碰到这种sleep()的或者其他IO操做,咱就切换到其他协程上(我们直到IO操作是交给操作系统的IO操作接口进行处理的,程序只要到操作系统注册一个IO任务,操作系统就会做后续的操作,最终把结果放到操作系统中等待返回给注册的程序),这样执行其他协程的代码,等其他协程有IO操作时,在切换到这个协程上取结果,这样就把本来要阻塞的1秒中给用上了.
那么到操作系统中注册一个IO操作怎么实现?不会~,通过这个普通的yield没有办法实现,yield只能实现一个简单的并发,但是一遇到阻塞怎样把阻塞丢给操作系统.yield不行.
用Greenlet可以实现~
Greenlet可以实现底层的阻塞的任务丢给操作系统的队列,具体实现细节后面再讲,它是一个第三方模块.我们不用它,我们就简单看一下就可以,我们要讲更好更高级的东西
Greenlet具体代码如下:
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 5 from greenlet import greenlet 6 7 8 def test1(): 9 print 12 10 gr2.switch() 11 print 34 12 gr2.switch() 13 14 15 def test2(): 16 print 56 17 gr1.switch() 18 print 78 19 20 gr1 = greenlet(test1) 21 gr2 = greenlet(test2) 22 gr1.switch()
高级的来了~~
Gevent
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
安装gevent
$ cd /Library/Frameworks/Python.framework/Versions/3.5/bin/
$ pip3.5 install gevent
gevent模块实现协程一遇到IO操作和sleep切换的实例,代码如下:
1 #!/usr/bin/env python3.5 2 #__author__:‘ted.zhou‘ 3 ‘‘‘ 4 使用gevent模块创建协程代码实例 5 ‘‘‘ 6 import gevent 7 def foo(): 8 print(‘\033[32;1mRunning in foo\033[0m‘) 9 gevent.sleep(0) # 这里sleep(0)就是为了掩饰gevent协程遇到IO或者sleep就切换的特性 10 print(‘\033[32;1mExplicit context switch to foo again\033[0m‘) 11 12 def bar(): 13 print(‘Explicit context to bar‘) 14 gevent.sleep(0) 15 print(‘Implicit context switch back to bar‘) 16 17 gevent.joinall([ # gevent.joinall() 是等待所有执行完成的意思 18 gevent.spawn(foo), # gevent.spawn()是启动的意思 19 gevent.spawn(bar), 20 ])
协程异步非阻塞:
上面的代码只是通过sleep的形式看切换的效果,那么我们现在通过gevent来看在但线程下并发的下载几个页面.
首先我们要了解一个简单的模块urllib,就是一个可以简单爬网页的一个模块.
在python3.0使用 urllib 中的urlopen 导入: from urllib import urlopen
在python2.0使用urllib2 中的urlopen 导入: from urllib2 import urlopen
代码如下:
1 #!/usr/bin/env python3.5 2 #__author__:‘ted.zhou‘ 3 ‘‘‘ 4 使用gevent 和 urllib 在但线程中并发的抓取几个页面 5 ‘‘‘ 6 7 from gevent import monkey; monkey.patch_all() 8 import gevent 9 from urllib.request import urlopen 10 11 def f(url): 12 print(‘GET: %s‘ % url) 13 resp = urlopen(url) #使用urlopen直接讲输入进来的URL进行下载 14 data = resp.read() # 把结果读取下来 15 print(‘%d bytes received from %s.‘ % (len(data), url)) #打印爬到的字节 16 17 # 使用gevent启动3个协程 18 gevent.joinall([ 19 gevent.spawn(f, ‘https://www.python.org/‘), #调用f函数,后面是参数 20 gevent.spawn(f, ‘https://www.yahoo.com/‘), 21 gevent.spawn(f, ‘https://github.com/‘), 22 ]) 23 执行结果: 24 GET: https://www.python.org/ 25 GET: https://www.yahoo.com/ 26 GET: https://github.com/ 27 451842 bytes received from https://www.yahoo.com/. 28 47381 bytes received from https://www.python.org/. 29 25540 bytes received from https://github.com/. 30 31 Process finished with exit code 0
从输出结果的顺序我们清楚的看到,一遇到爬页面的动作,就切换了.
运行过程如下:
当启动程序中的多个协程,遇到爬页面的请求,请求到操作系统注册一个IO请求后,就切换到第二,第三.
这三个请求发送到远端,远端通过网络返回数据.
远端通过网络返回给这台机器网络接口,网络接口数据进来的时候,网卡会通知CPU,对CPU产生一个中断请求,CPU接收到请求,就会通知操作系统说有数据来了,你要去接数据,操作系统就会把这个数据接回来,通知给当前具体的程序.程序内部会根据自己的寄存器\上下文\堆栈,把这个数据返回给对应的协程.
下面我们使用gevent实现一个更有用的代码实例
通过gevent实现单线程下的多socket并发
我们刚讲socket的,是一对一的,同时一个socket只能跟一个socket客户端,其他socket客户端都得排队等待.为了解决这个问题,实现一个多并发的效果,我们后来把单个socket改成了多线程的socketserver,改成多线程socket之后,是不是每一个客户端连过来的时候,socket server都会给这个连接分配一个新的线程跟这个客户端联系.这是低效的,为什么是低效的?
当有100个客户端连到多线程socketserver进行连接,但是连接之间的数据传输不多,那么就会产生上百个线程存在,但是实际用的不多.同时CPU还要不断的去检测socket客户端有没有传输数据.总体来说,开销很大,效率很低.
如果在线程下实现一个多socket,就像抓网页,在单线程下,但是每一个socket客户端过来我给你创建一个实例.不是每一个实例都是活跃,只有一小部分活跃.其他不活跃的就简单扫一遍,就略过,如果在线程下,只需要维护一个线程实现跟上百个socket通信.
如果想实现单线程下实现跟上百个socket客户端通信的效果.就必须解决如果一个客户端和我通信要10分钟,而其他实例要是也需要通信就得等待(阻塞)问题?
为了解决这个问题,客户端实例没有资格和socket服务端通信,在服务端前面前放一个纸箱子,服务端不断得轮询纸箱子看看有没有纸条,如果客户端需要和服务端说话,写一个纸条放入到纸箱子.当服务端看到纸条,就返回一个"已阅"得确认信息,这样每一个客户端传过来得服务端就都可以看到了.那这个但线程下得多socket就是这个效果.
代码中如何实现得呢?代码如下:
1 server side 2 #!/usr/bin/env python3.5 3 #__author__:"ted.zhou" 4 ‘‘‘ 5 单线程下实现多socket,解决多线程socketserver开销大,效率低得问题 6 ‘‘‘ 7 import sys 8 import socket 9 import time 10 import gevent 11 from gevent import monkey # 非常有意思,python中得黑魔法 12 from gevent import socket # 导入的是gevent下的socket 13 monkey.patch_all() # 非常有意思,python中得黑魔法,我们写socket是不是很多是阻塞得,比如读IO,或网络接口都是阻塞得,而这个monkey.path_all()方法就使得代码一旦遇到阻塞就切换到其他线程.也就是标题所说的(非阻塞). 14 def server(port): 15 s = socket.socket() 16 s.bind((‘0.0.0.0‘, port)) 17 s.listen(500) # 最多可以监听500个连接 18 while True: 19 cli, addr = s.accept() 20 gevent.spawn(handle_request, cli) # 启动一个新的协程,把客户端的socket对象,调用 21 def handle_request(s): 22 try: 23 while True: # 一个循环, 24 data = s.recv(1024) # 前面用了monkey.path_all()了,这里程序就不阻塞了,而是切换到其他线程了,这里就是回到调用它的server函数 25 print("recv:", data) 26 s.send(data) 27 if not data: # 如果没有数据 28 s.shutdown(socket.SHUT_WR) # 这个shutdown,就是把客户端连接过来产生的socket客户端对象销毁掉. 29 30 except Exception as ex: 31 print(ex) 32 finally: 33 34 s.close() # 把服务器跟这个客户端连接的实例关掉 35 if __name__ == ‘__main__‘: 36 server(8001)
client side 代码如下:
1 #!/usr/bin/env python3.5 2 #__author__:‘ted.zhou‘ 3 ‘‘‘ 4 socket客户端代码,就是普通的客户端 5 ‘‘‘ 6 7 import socket 8 9 HOST = ‘localhost‘ # The remote host 10 PORT = 8001 # The same port as used by the server 11 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 12 s.connect((HOST, PORT)) 13 while True: 14 msg = bytes(input(">>:"),encoding="utf8") 15 s.sendall(msg) 16 data = s.recv(1024) 17 #print(data) 18 19 # print(‘Received‘, repr(data)) 20 print(‘Received‘, data.decode()) 21 s.close()
同样你可以在client程序端,进行多线程的并发测试,看看server端能否正常应答,建议测试3000并发.
论事件驱动与异步IO
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。
让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。
以上是关于python2.0_s12_day9_协程&Gevent协程的主要内容,如果未能解决你的问题,请参考以下文章
python2.0_s12_day10_Twsited异步网络框架