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异步网络框架

python2.0_s12_day10_rabbitMQ使用介绍

python2.0_s12_day21_web聊天室一

day9-复习学习python实例

Python--day9--进程/线程/协程

python-学习 协程函数 模块与包