万物不为我所有,但为我所用|

园龄:粉丝:关注:

同步与异步

多路复用概念

监听多个描述符(文件描述符(windows下暂不支持)、网络描述符)的状态,如果描述符状态改变则会被内核修改标志位,进而被进程获取进行读写操作

I/O多路复用

简单解释:一个进程(线程)可以同时对多个客户请求进行服务

  1. 用于提升效率,单个进程可以同时监听多个网络连接IO
  2. 监视多个文件描述符,一旦描述符就绪(读就绪和写就绪),能通知程序进行相应的读写操作。
  3. I/O多路复用避免阻塞在io上,原本为多进程或多线程来接收多个连接的消息变为单进程或单线程保存多个socket的状态后轮询处理.

同步与异步

同步IO

  1. 阻塞 I/O(blocking IO):只读取一次数据,等待至有数据并返回OK,结束等待(准备数据时会阻塞,内核到用户会阻塞)

    blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了

    当服务器需要处理1000个连接的的时候,而且只有很少连接忙碌的,那么会需要1000个线程或进程来处理1000个连接,而1000个线程大部分是被阻塞起来的

    image

  2. 非阻塞 I/O(nonblocking IO):重复地去读数据,有数据就返回OK,没有就返回NULL或者异常,总之必须有返回结果(准备数据时不会阻塞,内核到用户会阻塞)

    在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。

    但还是需要1000个线程或进程处理连接,而且要不断的轮询来读取或写入

    image

  3. I/O 多路复用( IO multiplexing):等待的时候监听,当有数据可读就返回可读状态;当获取到可读状态则进行系统调用进行读取,返回OK(准备数据时不会阻塞,内核到用户会阻塞)“复用”指的是复用同一个线程

    多路复用是指使用一个线程来检查多个文件描述符(Socket、FD)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。这样在处理1000个连接时,只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了(即有几个就绪就开几个线程),这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。

    image

这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为它不仅阻塞了还多需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom),当只有一个连接请求的时候,这个模型还不如阻塞IO效率高。但是,用select的优势在于它可以同时处理多个connection,而阻塞IO那里不能,我不管阻塞不阻塞,你所有的连接包括recv等操作,我都帮你监听着,其中任何一个有变动(有链接,有数据),我就告诉用户,那么用户就可以去调用这个数据了,这就是他的NB之处

强调:

1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

为什么会发生阻塞呢?

其根本原因就是当数据准备好之后,内核态向用户态转发时呼叫进程来接收,这个转化的过程是耗时的操作,等转发结束后,阻塞接收可以用IO多路复用写一个SocketServer实现单线程下的大并发,但是IO多路复用本质还是同步IO,因为数据从内核态到用户态的拷贝需要等待操作系统完成

异步IO(asynchrous IO)

系统调用后立刻返回,没有任何状态,让数据的准备和复制在后台运行,主进程则去处理其他事情,同时等待数据准备完毕的信号,当接收到信号后再处理数据(期间没有等待)

image

为什么异步是没有阻塞的操作呢?

其根本原因也是因为,在内核态到用户态转发结束后,才叫进程来接收,进程不用等待,直接拿到数据

所以只有同步阻塞同步非阻塞情况,没有异步阻塞和异步非阻塞情况,异步只是异步

image

IO多路复用的机制:

  • select: Windows、Linux
  • poll: Linux #和select监听机制一样,但是对监听列表里面的数量没有限制,select默认限制是1024个,但是他们两个都是操作系统轮询每一个被监听的文件描述符(如果数量很大,其实效率不太好),看是否有可读操作。
  • epoll: Linux #它的监听机制和上面两个不同,他给每一个监听的对象绑定了一个回调函数,你这个对象有消息,那么触发回调函数给用户,用户就进行系统调用来拷贝数据,并不是轮询监听所有的被监听对象,这样的效率高很多。

epoll

被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

水平触发

epoll默认事件模型就是水平触发,即监控到fd可读写时,就会触发并且返回fd

例如fd可读时,但是使用recv没有全部读取完毕,那下次还会将fd触发返回,相对而言,这个更安全一些

边缘触发

就是每次只要触发一次我就会给你返回一次,即使你处理完成一半,我也不会给你返回了,除非他下次再次发生一个事件

文件句柄

f = open('filename','r',encoding='utf-8'),这个 f 就是文件句柄

import select #导入select模块
epoll = select.epoll() #创建一个epoll对象
epoll.register("文件句柄","事件类型") #注册要监控的文件句柄和事件
"""
事件类型:
  select.EPOLLIN 可读事件
  select.EPOLLOUT 可写事件
  select.EPOLLERR 错误事件
  select.EPOLLHUP 客户端断开事件
"""
epoll.unregister("文件句柄") #销毁文件句柄
epoll.poll(timeout)
"""
当文件句柄发生变化,则会以列表的形式主动报告给用户进程
timeout为超时时间,默认为-1,即一直等待直到文件句柄发生变化
如果指定为1,那么epoll每1秒汇报一次当前文件句柄的变化情况,如果无变化则返回空
"""
epoll.fileno() # 返回epoll的控制文件描述符
epoll.modfiy(fineno,event)
"""
fineno为文件描述符
event为事件类型:作用是修改文件描述符所对应的事件
"""
epoll.fromfd(fileno) # 从1个指定的文件描述符创建1个epoll对象
epoll.close() # 关闭epoll对象的控制文件描述符

示例

Client:

import socket
#创建客户端socket对象
clientsocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
##客户端连接指定的IP地址和端口号
server_address = ('127.0.0.1',8888)
clientsocket.connect(server_address)
while True:
#输入数据
data = raw_input('please input:')
#客户端发送数据
clientsocket.sendall(data)
#客户端接收数据
server_data = clientsocket.recv(1024)
print '客户端收到的数据:'server_data
#关闭客户端socket
clientsocket.close()

Server:

import socket
import select
import Queue
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #创建socket对象
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #设置socket复用
#绑定ip地址和端口号
server_address = ("127.0.0.1", 8888)
serversocket.bind(server_address)
serversocket.listen(10) #监听,并设置最大连接数
print "服务器启动成功,监听IP:" , server_address
serversocket.setblocking(False) #服务端设置非阻塞
timeout = 10 #超时时间
#创建epoll事件对象,后续要监控的事件添加到其中,格式为{句柄:对象}
epoll = select.epoll()
epoll.register(serversocket.fileno(), select.EPOLLIN)
message_queues = {}
fd_to_socket = {serversocket.fileno():serversocket,}
while True:
print "等待活动连接......"
#轮询注册的事件集合,返回值为[(文件句柄,对应的事件),(...),....]
events = epoll.poll(timeout)
if not events:
print "epoll超时无活动连接,重新轮询......"
continue
print "有" , len(events), "个新事件,开始处理......"
for fd, event in events:
socket = fd_to_socket[fd]
#如果活动socket为当前服务器socket,表示有新连接
if socket == serversocket:
connection, address = serversocket.accept()
print "新连接:" , address
connection.setblocking(False) #新连接socket设置为非阻塞
epoll.register(connection.fileno(), select.EPOLLIN) #注册新连接fd到待读事件集合
#把新连接的文件句柄以及对象保存到字典
fd_to_socket[connection.fileno()] = connection
#以新连接的对象为键值,值存储在队列中,保存每个连接的信息
message_queues[connection] = Queue.Queue()
#关闭事件
elif event & select.EPOLLHUP:
print 'client close'
#在epoll中注销客户端的文件句柄
epoll.unregister(fd)
#关闭客户端的文件句柄
fd_to_socket[fd].close()
#在字典中删除与已关闭客户端相关的信息
del fd_to_socket[fd]
#可读事件
elif event & select.EPOLLIN:
#接收数据
data = socket.recv(1024)
if data:
print "收到数据:" , data , "客户端:" , socket.getpeername()
#将数据放入对应客户端的字典队列
message_queues[socket].put(data)
#修改读取到消息的连接到等待写事件集合(即对应客户端收到消息后,再将其fd修改并加入写事件集合)
epoll.modify(fd, select.EPOLLOUT)
#可写事件
elif event & select.EPOLLOUT:
try:
#从字典中获取对应客户端的信息
msg = message_queues[socket].get_nowait()
except Queue.Empty:
print socket.getpeername() , " queue empty"
#修改文件句柄为读事件
epoll.modify(fd, select.EPOLLIN)
else :
print "发送数据:" , data , "客户端:" , socket.getpeername()
#发送数据
socket.send(msg)
#在epoll中注销服务端文件句柄
epoll.unregister(serversocket.fileno())
#关闭epoll
epoll.close()
#关闭服务器socket
serversocket.close()

区别

调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。

synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,四个IO模型可以分为两大类:

  • blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO这一类
  • asynchronous I/O单独一类 。

有人可能会说,non-blocking IO并没有被阻塞。这里有个非常“狡猾”的地方,定义中所指的”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。

本文作者:注入灵魂

本文链接:https://www.cnblogs.com/totopian/p/16022370.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   注入灵魂  阅读(47)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起
  1. 1 尚好的青春 孙燕姿
尚好的青春 - 孙燕姿
00:00 / 00:00
An audio error has occurred.