本节内容
1、Gevent 协程
2、Select/poll/Epoll异步IO与事件驱动
3、RabbitMQ队列
一、 协程
1.1 协程概念
协程,又称微线程,英文名Coroutine,一句话说明什么是协程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
1.2 协程的优点
无需线程上下文切换的开销。
无需原子操作锁定及同步的开销。
原子操作:原子操作(atomic operation)是不需要synchronized,所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch(切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,或者切割掉只执行的部分,视作整体是原子性的核心。
方便切换控制流,简化编程模型。
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题,所以很适合用于高并发处理。
1.3 协程的缺点
无法利用多核资源:协程的本质是个单线程,它不能同时将单个CPU的多个核用上,协程需要和进程配合才能运行多CPU上,当然我们日常所编写的绝大部分应用都没有这个必要,除非是CPU密集型应用。
进行阻塞(blocking)操作(如IO时)会阻塞掉整个程序。
1.4 实现协程实例
1.4.1 使用yield实现协程操作例子
import time,queue def consumer(name): print('---->starting eating baozi....') while 1: new_baozi=yield print("[%s] is eating baozi %s"%(name,new_baozi)) def producer(): r=con.__next__() r=con2.__next__() n=0 while n <5: n +=1 con.send(n) con2.send(n) print("\033[32;1m [producer]\033[0m is making baozi %s"%n) if __name__ == '__main__': con=consumer('c1') con2=consumer('c2') p=producer()
看上面例子,这是一个简单利用生成器的方式实现了类似协程的功能,那么对于协程的定义是什么呢?符合什么条件才能称之为协程呢?
下面我们总结了一下:
1、必须在只有一个单线程里实现并发。
2、修改共享数据不需加锁。
3、用户程序里自己保存多个控制流的上下文栈。
4、一个协程遇到IO操作自动切换到其他协程。
这样看来,使用生成器实现的协程根本意义上实现了1-3的条件,没有实现条件4,根本意义上不算是协程。
1.4.2 Greenlet
greenlet是一个用C实现的协程模块,相比与python自带的yield,他可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator
#/usr/bin/env python #-*- conding:utf-8 -*- from greenlet import greenlet def test1(): print(12) gr2.switch() print(14) gr2.switch() def test2(): print(22) gr1.switch() print(24) gr1.switch() gr1=greenlet(test1) gr2=greenlet(test2) gr1.switch()
感觉确实用着比generator(生成器)还简单了呢,但是好像还没解决一个问题,就是遇到IO操作,自动切换的问题。。
1.4.3 Gevent
Gevent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到主要模式是Greenlet,它是以C扩展模块形式接入Python轻量级协程,Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
import gevent def func1(): print("\033[31;1m 刘强在跟沁夫搞....\033[0m") gevent.sleep(2) print('\033[31;1m 刘强又回来继续跟沁夫搞....') def func2(): print('\033[32;1m 刘强切换到跟白龙搞...\033[0m') gevent.sleep(1) print('\033[32;1m 刘强搞完白龙,回去继续跟沁夫搞....\033[0m') gevent.joinall([ gevent.spawn(func1), gevent.spawn(func2), ]) 结果: 刘强在跟沁夫搞.... 刘强切换到跟白龙搞... 刘强搞完白龙,回去继续跟沁夫搞.... 刘强又回来继续跟沁夫搞....
1.4.4 同步与异步的性能区别
import gevent def task(pid): gevent.sleep(0.5) print('Task %s done '% pid) def syn(): for i in range(1,10): task(i) def asyn(): threads=[gevent.spawn(task,i) for i in range(10)] gevent.joinall(threads) print('Syn:') syn() print('Asyn:') asyn()
上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。初始化的Greenlet列表存放在数组threads中,此数组被传给gevent.joinall函数,后者阻塞当前流程,并执行所有给定的Greenlet,执行流程只会在所有Greenlet执行完后才会继续向下走。
1.4.5 遇到IO阻塞时会自动切换任务
import gevent from gevent import monkey monkey.patch_all() from urllib.request import urlopen import time def pa_web_page(url): print("GET url",url) req = urlopen(url) data =req.read() print(data) print('%d bytes received from %s.' % (len(data), url)) t_start = time.time() pa_web_page("http://www.autohome.com.cn/beijing/") pa_web_page("http://www.xiaohuar.com/") t2_start = time.time() gevent.joinall([ gevent.spawn(pa_web_page, 'http://www.autohome.com.cn/beijing/'), gevent.spawn(pa_web_page, 'http://www.xiaohuar.com/'), ]) print("\033[33;1m time cost:%s\033[0m"%(time.time()-t_start)) print("\033[32;1m time cost t2:%s\033[0m"%(time.time()-t2_start))
1.4.6 通过gevent实现单线程下的多socket并发
sever_side
import sys,socket,time,gevent from gevent import socket,monkey monkey.patch_all() def server(port): s=socket.socket() s.bind(('0.0.0.0',port)) s.listen(500) while 1: cli,addr = s.accept() gevent.spawn(handle_request,cli) def handle_request(conn): try: while 1: data=conn.recv(1024) print('recv:',data) conn.send(data) if not data: conn.shutdown(socket.SHUT_WR) except Exception as e: print(e) finally: conn.close() if __name__ == '__main__': server(8001)
client_side
import socket HOST='localhost' PORT=8001 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect((HOST,PORT)) while 1: msg=bytes(input('>>:'),encoding='utf8') s.sendall(msg) data=s.recv(1024) print('Received',repr(data)) s.close()
并发100个socket连接
import socket,threading def sock_conn(): client=socket.socket() client.connect(("localhost",8001)) count=0 while 1: client.send(('hello %s' % count).encode("utf-8")) data=client.recv(1024) print("[%s]recv from server:"% threading.get_ident(),data.decode()) count +=1 client.close() for i in range(100): t=threading.Thread(target=sock_conn) t.start()
二 、论事件驱动与异步IO
通常,我们写服务器处理模型的程序时,有以下几种模型:
1、每收到一个请求,创建一个新的进程,来处理该请求;
2、每收到一个请求,创建一个新的线程,来处理该请求;
3、每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求。
以上的几种方法,各有千秋,
第一种方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。
第二种方法,由于涉及到线程的同步,有可能会面临死锁等问题。
第三种方式,在写应用程序代码时,逻辑比前面两种都复杂。
综合考虑各种方面因素,一般普遍认为第三种方法是大多数网络服务器采用的方式。
看图说话讲事件驱动模型
在UI编程中,常常要对数据点击进行相应,首先如何获得鼠标点击呢?
方式一: 创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:
1、CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费,如果扫描鼠标点击的接口是阻塞的呢?
2、如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘。
3、如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
所以该方法非常不好。
方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
1. 有一个事件(消息)队列;
2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;
如图:
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。
让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。
如图:
在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。
在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。
在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。
当我们面对如下的环境时,事件驱动模型通常是一个好的选择:
程序中有许多任务,而且…
任务之间高度独立(因此它们不需要互相通信,或者等待彼此)而且…
在等待事件到来时,某些任务会阻塞。
当应用程序需要在任务间共享可变的数据时,这也是一个不错的选择,因为这里不需要采用同步处理。
网络应用程序通常都有上述这些特点,这使得它们能够很好的契合事件驱动编程模型。
此处要提出一个问题,就是,上面的事件驱动模型中,只要一遇到IO就注册一个事件,然后主程序就可以继续干其它的事情了,只到io处理完毕后,继续恢复之前中断的任务,这本质上是怎么实现的呢?哈哈,下面我们就来一起揭开这神秘的面纱。。。。
三、Select\Poll\Epoll异步IO
1.1 三者的区别
select
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个学点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这个限制。。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长,同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
poll
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制与用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,他的开销随着文件描述符数量的增加而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切有点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知哪些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在与epoll采用基于事件的就绪通知方式,在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而Epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
Python select
Python的select()方法直接调用操作系统的IO接口,它监控socket,open files,andpipes(所有带fileno()方法的文件句柄)何时变成readable和writeable,或者通信错误,select()使得同时监控多个连接变的简单,并且这比写一个长循环来等待和监控对客户端连接要高效,因为select直接通过操作系统提供的c的网络接口进行操作,而不是通过Python的解释器。
接下来通过echo server例子要以了解select是如何通过单进程实现同时处理多个非阻塞的socket连接的。
select_socket_server
import select,socket,sys,queue server=socket.socket() server.setblocking(0) server_addr=('localhost',8001) print('starting up on %s port %s'%server_addr) server.bind(server_addr) server.listen(5) inputs=[server,]#自己也要监测呀,因为server本身也是个fd outputs=[] message_queues={} while 1: print("waiting for next event....") readable,writeable,exeptional=select.select(inputs,outputs,inputs) #如果没有任何fd就绪,那程序就会一直阻塞在这里 for s in readable: #每个s就是一个socket if s is server:#别忘记,上面我们server自己也当做一个fd放在了inputs列表里,传给了select,如果这个s是server,代表server这个fd就绪了。 #就是有活动了,什么情况下它才有活动?当然是有新连接进来的时候呀。 #新连接捡来了,接受这个连接。 conn,client_addr=s.accept() print("new connection from",client_addr) conn.setblocking(0) inputs.append(conn)#为了不阻塞整个程序,我们不会立刻在这里开始接受客户端发来的数据,把它放到inputs里,下一次loop时,这个新连接。 #就会被交给select去监听,如果这个连接的客户端发来了数据,那个连接的fd在server端就会变成就续的,select就会把这个连接返回,返回到 #readable列表里,然后你就可以loop readable列表,取出这个连接,开始接受数据了,下面就是这么干的。 message_queues[conn]=queue.Queue() #接收到客户端的数据后,不立刻返回,暂存在队列里,以后发送。 else:#如果s不是server的话,那就只能是一个,与客户端建立连接的fd了 #客户端的数据过来了,在这里接收 data=s.recv(1024) if data: print("收到来自[%s]的数据:"%s.getpeername()[0],data) message_queues[s].put(data)#收到的数据先放到queue中,一会返回给客户端。 if s not in outputs: outputs.append(s)#为了不影响处理与其它客户端的连接,这里不立刻返回数据给客户端。 else:#如果收不到data代表什么呢?代表客户端断开了连接 print("客户端断开了",s) if s in outputs: outputs.remove(s)#清理已断开的连接 inputs.remove(s)#清理已断开的连接 del message_queues[s]#清楚已断开的连接 for s in writeable: try: next_msg=message_queues[s].get_nowait() except queue.Empty: print("client [%s]"%s.getpeername()[0],"queue is empty...") outputs.remove(s) else: print("sending msg to [%s]"%s.getpeername()[0],next_msg) s.send(next_msg.upper()) for s in exeptional: print("handling excepion for ",s.getpeername()) inputs.remove(s) if s in outputs: outputs.remove(s) s.close() del message_queues[s]
select_scoket_client
import socket,sys messages = [ b'This is the message.', b'It will be sent', b'in parts.', ] server_adderss=('localhost',8001) socks=[ socket.socket(socket.AF_INET,socket.SOCK_STREAM), socket.socket(socket.AF_INET,socket.SOCK_STREAM), ] print('connecting to %s port %s'%server_adderss) for s in socks: s.connect(server_adderss) for message in messages: for s in socks: print('%s:sending "%s"'%(s.getsockname(),message)) s.send(message) for s in socks: data=s.recv(1024) print('%s:received "%s"'%(s.getsockname(),data)) if not data: print(sys.stderr,'closing socket',s.getsockname())
selectors模块
此模块允许高级和高效的I/O多路复用,构建在select模块之上,强烈用户使用此模块。
import selectors,socket sel=selectors.DefaultSelector() def accept(sock,mask): conn,addr=sock.accept() print('accepted',conn,'from',addr) conn.setblocking(False) sel.register(conn,selectors.EVENT_READ,read) def read(conn,mask): data=conn.recv(1024) if data: print('echoing',repr(data),'to',conn) conn.send(data) else: print('closing',conn) sel.unregister(conn) conn.close() sock=socket.socket() sock.bind(('localhost',8001)) sock.listen(100) sock.setblocking(False) sel.register(sock,selectors.EVENT_READ,accept) while 1: events=sel.select() for key,mask in events: callback=key.data callback(key.fileobj,mask)