文章目录
1、 io 操作不暂用CPU
IO操作,即对硬盘上的数据进行读写操作。
计算占用CPU(所以大量的计算,在python中一般不用多线程)
Python的多线程不适合CPU密集操作型的任务,适合io操作密集型的任务
2、多进程
也是用的原生系统的多进程,每个进程是独立的。所有也没有GIL的概念,同时原生系统也没有GIL,所以可以利用多核(每个进程对应一个内核,然后每个进程里面有一个线程),可以折中的解决python不能利用多核的问题。
3、multiprocessing 多进程
语法和线程基本一样
import multiprocessing,time
def run(name):
time.sleep(2)
print("hello:%s"%name)
if __name__ == '__main__':
for i in range(10):
t= multiprocessing.Process(target=run,args=(i,))
t.start()
同时在一个进程里面还可以运行一个线程:
import multiprocessing,time,threading
def thread_id():
print(threading.get_ident())#获得运行的线程号
def run(name):
time.sleep(2)
print("hello:%s"%name)
t=threading.Thread(target=thread_id)
t.start()
if __name__ == '__main__':
for i in range(10):
t= multiprocessing.Process(target=run,args=(i,))
t.start()
4、获得进程id
每一个子进程都是由父进程启动的
import multiprocessing
import os
def info(title):
print(title)
print('module name',__name__)
print('parent process:',os.getppid())
print('process id:',os.getpid())
print("\n\n")
def f(name):
info('\033[31;1mfunction f\033[0m')
print('hello',name)
if __name__ == '__main__':
info('\033[32;1mmain process line\033[0m')
p= multiprocessing.Process(target=f,args=('bob',))
p.start()
p.join()
运行结果:主进程也是有父进程的,在pycharm中,父线程就是pycharm
在Ubuntu中,父线程是Termina
l
5、进程之间的数据的传递
5.1Queues 这是进程里面的
mport multiprocessing
def f(q):
q.put([42,None,'sf'])
if __name__ =='__main__':
q = multiprocessing.Queue()
p = multiprocessing.Process(target=f,args=(q,))
p.start()
print(q.get())
p.join()
或者
from multiprocessing import Process,Queue
def f(q):
q.put([42,None,'sf'])
if __name__ =='__main__':
q = Queue()
p = Process(target=f,args=(q,))
p.start()
print(q.get())
p.join()
这里面进行通信的两个对列不是同一个队列,二者中间通过一个机制来进行数据传递的。
5.2 Pipes
函数的作用是:返回一对由管道连接的连接对象,默认情况下管道是双工(双向)的
from multiprocessing import Process,Pipe
def f(conn):
conn.send([42,None,'dgs'])
print(conn.recv())
conn.close()
if __name__ == '__main__':
parent_conn,child_conn = Pipe()
p = Process(target=f,args=(child_conn,))
p.start()
print(parent_conn.recv())
parent_conn.send('from parent')
p.join()
send
和recv
一定要成对出现(类似于socket通信)
6、进程之间数据的共享
manager()返回的manager对象控制一个服务器进程,该进程持有Python对象,并允许其他进程使用代理操作它们
manager()有以下多种操作对象
dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array
from multiprocessing import Process,Manager
import os
def f(d,l):
d[os.getpid()] = os.getpid()
l.append(os.getpid())
if __name__ == '__main__':
with Manager() as manger:
d = manger.dict()
l = manger.list(range(5))
p_list=[]
for i in range(10):
p = Process(target=f,args=(d,l))
p_list.append(p)
p.start()
for p1 in p_list:
p1.join()
print(d)
print(l)
7、进程锁
如果不使用来自不同进程的锁输出,则很可能会混淆。
from multiprocessing import Process,Lock
def f(l,i):
l.acquire()
print('hello woed',i)
l.release()
if __name__ == '__main__':
lock = Lock()
for num in range(10):
Process(target=f,args=(lock,num)).start()
8、if __ name__ == ‘__ main__’:
就是为了判断是手段执行这个脚本,还是当做模块执行,如果是当做模块执行,则这个下面的代码不会执行。name 可以返回模块名
9、进程池
进程池内部维护了一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有两种方法:
apply 表示串行
apply_async 表示并行
from multiprocessing import Pool
import time
import os
def foo(i):
time.sleep(2)
print('in process',os.getpid())
return i+100
def Bar(arg): #回调
print('-->exec done:',arg,os.getpid())
if __name__ == '__main__':
pool = Pool(5) #允许进程池同时放入5个进程
print("主进程",os.getpid())
for i in range(10):
# pool.apply(func=foo,args=(i,)) #串行
pool.apply_async(func=foo,args=(i,),callback=Bar) #callback=Bar 是回调函数,
# 即执行一个进程结束后,调用这个函数,同时是主进程调用的这个回调函数
# (可以应用于多个进程写日志,由主线程打开日志,执行一个写入一个日志,
#而不是在每个子进程中写日志,这样会降低程序的效率)
print('end')
pool.close() #一定是先关闭进程池,再调用join()函数
pool.join() #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭
10、协程(就是在单线程里使用的)
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
10.1 协程的好处:
1)无需线程上下文切换的开销
2)无需原子操作锁定及同步的开销
“原子操作(atomic operation)是不需要synchronized”,所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch(切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
3)方便切换控制流,简化编程模型
4)高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题,所以很适合用于高并发处理。
10.2协程的缺点:
1)无法利用多核资源:协程的本质是个单线程,它不能同时将单个CPU的多个核用上,协程需要和进程配合才能运行在多CPU上,当然我们日常所编写的绝大部分应用都没有这个必要,除非是CPU密集型应用。
2)进程阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
10.3利用yield实现协程
在程序中遇到IO操作就切换(IO比较耗时),这样整个程序就变成CPU运算(快)
问题是:什么时候切换回去?
def consumer(name):
print("---->strating eating baozi---")
while True:
new_baozi = yield
print('[%s] is eating baozi %s'%(name,new_baozi))
def produce(con,con2):
con.__next__()
con2.__next__()
n = 0
while n<5:
n+=1
con.send(n)
con2.send(n)
print("\033[32;1m[produce]\033[0m is making baozi %s"%n)
if __name__ == '__main__':
con= consumer("c1")
con2 = consumer("c2")
p=produce(con,con2)
协程的一个标准定义,即符合什么条件就能称之为协程:
1)必须在只有一个单线程里实现并发
2)修改共享数据不需要加锁
3)用户程序里自己保持多个控制流的上下文栈
4)一个协程遇到IO操作自动切换到其它协程
10.4用Greenlet 实现协程
Greenlet是一个用C实现的协程模块,相比于python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator。
from greenlet import greenlet
def test1():
print(12)
gr2.switch()
print(34)
gr2.switch()
def test2():
print(56)
gr1.switch()
print(78)
gr1 = greenlet(test1) #启动一个协程
gr2 = greenlet(test2)
gr1.switch()
结果:
10.5 Gevent
Gevent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet,它是以C扩展模块新式接入python的轻量级协程。Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调用。
import gevent
def func1():
print('excute in func1....')
gevent.sleep(2) #跳转
print('excute in func1 again....')
def func2():
print('excute in func2....')
gevent.sleep(1) #跳转
print('excute in func2 again....')
def func3():
print('excute in func3....')
gevent.sleep(0) #跳转
print('excute in func3 again....')
gevent.joinall([gevent.spawn(func1),#生成
gevent.spawn(func2),
gevent.spawn(func3)])
运行结果 其中在执行excute in func2 again.... 和excute in func1 again....时中间会有停顿,因为gevent.sleep的原因,就是模拟有IO操作就跳转
。
11、同步和异步的性能区别:
import gevent
def task(pid):
"""
Some non-deterministic task
"""
gevent.sleep(0.5)
print('Task %s done' % pid)
def synchronous():
for i in range(1, 10):
task(i)
def asynchronous():
threads = [gevent.spawn(task, i) for i in range(10)]
gevent.joinall(threads)
print('Synchronous:')
synchronous() #同步执行
print('Asynchronous:')
asynchronous() #异步执行
12、gevent 实现简单的异步爬虫
import gevent,time
from gevent import monkey
from urllib import request
monkey.patch_all() #把当前程序的所有IO操作单独的做上标记
def f(url):
print('get:%s'%url)
resp = request.urlopen(url)
data = resp.read()
print("%d bytes received from %s"%(len(data),url))
with open('a','wb') as f:
f.write(data)
url = 'https://www.cnblogs.com/alex3714/articles/5248247.html'
ur2 = 'https://www.cnblogs.com/liujiacai/p/7417953.html'
ur3 = 'https://www.apeland.cn/python'
u=[url,ur2,ur3]
syn_time = time.time() #同步执行
for i in u:
f(i)
print("syn cost time:",time.time()-syn_time)
asy_time = time.time() #异步执行
gevent.joinall([gevent.spawn(f,url),#启动协程
gevent.spawn(f,ur2),
gevent.spawn(f,ur3)])
print("asy cost time:",time.time()-asy_time )
结果:
get:https://www.cnblogs.com/liujiacai/p/7417953.html
33167 bytes received from https://www.cnblogs.com/liujiacai/p/7417953.html
get:https://www.apeland.cn/python
45995 bytes received from https://www.apeland.cn/python
syn cost time: 0.8250470161437988
get:https://www.cnblogs.com/alex3714/articles/5248247.html
get:https://www.cnblogs.com/liujiacai/p/7417953.html
get:https://www.apeland.cn/python
93444 bytes received from https://www.cnblogs.com/alex3714/articles/5248247.html
33167 bytes received from https://www.cnblogs.com/liujiacai/p/7417953.html
45995 bytes received from https://www.apeland.cn/python
asy cost time: 0.37002110481262207
13、gevent 实现socket 多并发
import gevent
from gevent import socket,monkey
monkey.patch_all()
def server(port):
s = socket.socket()
s.bind(('localhost',port))
s.listen(500)
while True:
conn,addr = s.accept()
gevent.spawn(handle_request,conn)#启动一个协程
def handle_request(conn):
try:
while True:
data = conn.recv(1024)
print('recv:',data.decode())
conn.send(data.upper())
if not data:
conn.shutdown(socket.SHUT_WR)
except Exception as ex:
print(ex)
finally:
conn.close()
if __name__ == '__main__':
server(9999)
14、事件驱动和异步IO
通常,我们写服务器处理模型的程序时,有以下几种模型:
1)每收到一个请求,创建一个新的进程,来处理该请求;
2)每收到一个请求,创建一个新的线程,来处理该请求;
3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞IO方式来处理请求
上面的几种方式,各有千秋。
第(1)中方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。
第(2)中方法,由于要涉及到线程的同步,有可能会面临死锁等问题。
第(3)中方法,在写应用程序代码时,逻辑比前两种都复杂
综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式。
在UI编程中,常常要对鼠标点击进行响应,首先如何获得鼠标点击呢?
方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:
1)CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?
2)如果是阻塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
3)如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
所以,该方式是非常不好的。
方式二:就是时间驱动模型
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
1)有一个事件(消息)队列
2)鼠标按下时,往这个队列中增加一个点击事件(消息)
3)有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
4)事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数。
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式(单线程)同步以及多线程编程。
让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有三个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。
15、IO 多路复用
15.1用户空间和内核空间
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对Linux操作系统而言,将最高的1G字节(从虚拟地址0XC0000000)到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址(0x00000000到0xBFFFFFFF)),供各个进程使用,称为用户空间。
15.2 进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程运行转到另一个进程上运行,这个过程中经过下面这些变化:
1)保存处理机上下文,包括程序计数器和其他寄存器
2)更新PCB信息
3)把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列
4)选择另一个进程执行,并更新其PCB
5)更新内存管理的数据结构、
6)恢复处理机上下文
总而言之就是很耗资源。
15.3 进程的阻塞
正在执行的进程,由于期待的某些事情未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或五新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为。也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
15.4 文件描述符fd
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数,实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
15.5 缓存 I/O
缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。
15.6 IO 模式
刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
1)等待数据准备(waiting for the data to be ready)
2)将数据从内核拷贝到进程中(copying the data from the kernel to the process)
正是因为这两个阶段,Linux系统产生了下面五种网络模式的方案。
阻塞I/O(blocking IO)
非阻塞I/O(nonblocking IO)
I/O多路复用(IO multiplexing)
信号驱动 I/O(signal driven to)
异步 I/O(asynchronous IO)
阻塞 I/O(blocking IO)
在Linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概就是这样:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中需要一个过程的。而在用户进程这边,整个进程会阻塞(当然,是进程自己选择的阻塞)。当kernal一直等待数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
非阻塞 I/O(nonblocking IO)
Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发生read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,nonblocking IO的特定是用户进程需要不断的主动询问kernel数据好了没有。
I/O多路复用(IO multiplexing)
IO multiplexing 就是我们说的select poll epoll ,有些地方也称这种IO方式为event driven IO。Select/epoll 的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select poll epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
所以,I/O多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些,因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能好,可能延迟还更大。Select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
异步 I/O(asynchronous IO)
Linux下的asynchronous IO其实用得很少。先看一下它的流程:
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
总结:
Blocking和non-blocking的区别
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
Synchronous IO和asynchronous IO的区别
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSX的定义是这样子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的的区别就在于synchronous IO做“IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO
有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。
而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
各个IO Model的比较如图所示:
通过上面的图片,可以发现non-blocking IO和asynchronous IO区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
16 python select解析
首先列一下,sellect、poll、epoll三者的区别
1)select
Select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进程后续的读写操作。
Select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的有点之一。
Select的一个缺点在于单进程能够监视的文件描述符的数量存在最大限制。在linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
2)poll
Poll在1986年诞生于system V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
Poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增大而线性增大。
另外。Select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(level triggered)
3)epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
Epoll可以同时支持水平触发和边缘触发(edge triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
Epoll 同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定一个数量中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪后,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进行调用epoll_wait()时便会得到通知。
17 python select
Python的select()方法直接调用操作系统的IO接口,它监视sockets,open files,and pips(所有带fileno()方法的文件句柄)何时变成readable和writeable,或者通信错误,select()使得同时监听多个连接变的简单,并且这比写一个长循环来等待和监控多客户端连接要高效,因为select直接通过操作系统提供的C的网络接口进行操作,而不是通过python的解释器。
import socket
import select
import queue
server = socket.socket()
server.bind(('localhost',9999))
server.listen()
server.setblocking(False) #s设置为不阻塞
msg_dic = {}
input =[server,]
output = []
while True:
readable,writeable,exceptional = select.select(input,output,input)
print(readable,writeable,exceptional)
for r in readable:
if r is server: #代表来了一个新连接
conn,addr = server.accept()
print("来个一个新连接:",addr)
print(conn)
input.append(conn) #是因为这个新建立的连接还没有发数据过来,现在就接收的话程序就报错了
#所有要想实现这个客户端发送数据时server端能知道,就需要让select再监测这个conn
msg_dic[conn] = queue.Queue() #初始化一个队列,后面存要返回给这个客户端的数据
else:
data = r.recv(1024)
print("收到数据",data)
msg_dic[r] .put(data)
output.append(r) #放入返回的连接队列里
for w in writeable: #要返回给客户端的连接列表
data_to_client = msg_dic[w].get()
w.send(data_to_client) #返回给客户端的源数据
output.remove(w) #确保下次循环的时候writeable,不返回这个已经处理完的连接了
for e in exceptional:
if e in output:
output.remove(e)
input.remove(e)
del msg_dic[e]
18、selectors模块
这个模块允许高级和高效的I/O多路复用,建立在选择模块原语的基础上。我们鼓励用户使用这个模块,除非他们需要对所使用的os级原语进行精确控制。
import selectors
import socket
sel = selectors.DefaultSelector()
def accept(sock,mask):
conn,addr = sock.accept()
print("acceptd ",conn,'from',addr)
conn.setblocking(False)
sel.register(conn,selectors.EVENT_READ,read) #新连接注册read回调函数
def read(conn,mask):
data = conn.recv(1024)
if data:
print('echoing',repr(data),'to',conn)
else:
print('closing',conn)
sel.unregister(conn)
conn.close()
sock = socket.socket()
sock.bind(('localhost',9999))
sock.listen(1000)
sock.setblocking(False)
sel.register(sock,selectors.EVENT_READ,accept)
while True:
events = sel.select() #默认阻塞,有活动连接就返回活动的连接列表
for key,mask in events:
callback = key.data #accept
callback(key.fileobj,mask) #key.fileobj= 文件句柄