转载博客:python系列之 - select        深入理解python中的select模块

  I/O多路复用是在单线程模式下实现多线程的效果,实现一个多I/O并发的效果。

看一个简单socket例子:

#服务端
import socket  

sockServer = socket.socket()  
sockServer.bind(('127.0.0.1', 8001))  
sockServer.listen(5)  
  
while True:  
    conn, addr = sockServer.accept()  
    while True:  
        recvdata = conn.recv(1024)
        if recvdata:
            print(recvdata.decode())  
        else:  
            conn.close()  
            break

客户端:

import socket  
  
Client = socket.socket()  
Client.connect(('127.0.0.1', 8888))  
while True:  
    data = input("input str:")  
    Client.send(data.encode())   

  以上为一个简单的客户端发送一个输入信息给服务端的socket通信的实例,在以上的例子中,服务端是一个单线程、阻塞模式的。如何实现多客户端连接呢?我们可以使用多线程模式,这个当然没有问题。 使用多线程、阻塞socket来处理的话,代码会很直观,但是也会有不少缺陷。它很难确保线程共享资源没有问题。而且这种编程风格的程序在只有一个CPU的电脑上面效率更低。但如果一个用户开启的线程有限的情况下,比如1024个。当第1025个客户端连接是仍然会阻塞。

  有没有一种比较好的方式呢?当然有,其一是使用异步socket。这种socket只有在一些event触发时才会阻塞。相反,程序在异步socket上面执行一个动作,会立即被告知这个动作是否成功。程序会根据这个信息决定怎么继续下面的操作由于异步socket是非阻塞的,就没有必要再来使用多线程。所有的工作都可以在一个线程中完成。这种单线程模式有它自己的挑战,但可以成为很多方案不错的选择。它也可以结合多线程一起使用:单线程使用异步socket用于处理服务器的网络部分,多线程可以用来访问其他阻塞资源,比如数据库。

  Linux的2.6内核有一系列机制来管理异步socket,其中3个有对应的Python的API:select、poll和epoll。epoll和pool比select更好,因为Python程序不需要检查每一个socket感兴趣的event。相反,它可以依赖操作系统来告诉它哪些socket可能有这些event。epoll 比 pool 更好,因为它不要求操作系统每次都去检查python程序需要的所有socket感兴趣的event。而是Linux在event发生的时候会跟踪到,并在Python需要的时候返回一个列表。因此epoll对于大量(成千上万)并发socket连接,是更有效率和可扩展的机制。

异步I/O处理模型

Python 之 select 模块

非阻塞式I/O编程特点

  所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回;

  所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生,则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高。

  1、如果一个发现I/O有输入,读取的过程中,另外一个也有了输入,这时候不会产生任何反应.这就需要你的程序语句去用到select函数的时候才知道有数据输入。
  2、程序去select的时候,如果没有数据输入,程序会一直等待,直到有数据为止,也就是程序中无需循环和sleep。

Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用。

  Windows Python:提供: select

  Mac Python:提供: select

  Linux Python:提供: select、poll、epoll

select介绍

  select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
  select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
  select的一个缺点在于,单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。

  select主要用于socket通信当中,能监视我们需要的文件描述变化。select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组, 每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成, 当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读或可写。主要用于Socket通信当中。

  另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

  注意:网络操作、文件操作、终端操作等均属于IO操作,对于windows只支持Socket操作,其他系统支持其他IO操作,但是无法检测 普通文件操作 自动上次读取是否已经变化。进程指定内核监听哪些文件描述符(最多监听1024个fd)的哪些事件,当没有文件描述符事件发生时,进程被阻塞;当一个或者多个文件描述符事件发生时,进程被唤醒。

当我们调用select()时:
  1、上下文切换转换为内核态
  2、将fd从用户空间复制到内核空间
  3、内核遍历所有fd,查看其对应事件是否发生
  4、如果没发生,将进程阻塞,当设备驱动产生中断或者timeout时间后,将进程唤醒,再次进行遍历
  5、返回遍历后的fd
  6、将fd从内核空间复制到用户空间

select使用语法:

  【select可操作任何对象,但对象内必须是有fileno方法(文件句柄),其内部执行的操作就是对fileno对象做的监测。】

  句柄列表11(r), 句柄列表22(w), 句柄列表33(e) = select.select(句柄序列1, 句柄序列2, 句柄序列3, 超时时间)

  参数: 可接受四个参数(前三个必须)

  返回值:三个列表

  select方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。

    1、当 参数1 序列中的句柄发生可读时(accetp和read),则获取发生变化的句柄并添加到 返回值1 序列中;

    2、当 参数2 序列中含有句柄时,则将该序列中所有的 句柄 添加到 返回值2 序列中;

    3、当 参数3 序列中的句柄发生错误时,则将该发生错误的句柄添加到 返回值3 序列中;

    4、当 超时时间 未设置,则select会一直阻塞,直到监听的句柄发生变化。

    例如:当 超时时间 = 1时,那么如果监听的句柄均无任何变化,则select会阻塞 1 秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。

实例:利用select实现一个可并发的服务端

import socket
import select
 
s = socket.socket()
s.bind(('127.0.0.1',8888))
s.listen(5)
r_list = [s,]
num = 0
while True:
 rl, wl, error = select.select(r_list,[],[],10)
 num+=1
 print('counts is %s'%num)
 print("rl's length is %s"%len(rl))
 for fd in rl:
  if fd == s:
   conn, addr = fd.accept()
   r_list.append(conn)
   msg = conn.recv(200)
   conn.sendall(('first----%s'%conn.fileno()).encode())
  else:
   try:
    msg = fd.recv(200)
    fd.sendall('second'.encode())
   except ConnectionAbortedError:
    r_list.remove(fd)
 
 
s.close()
服务端

相关文章: