【并发编程】IO模型

 

一、要点回顾

为了更好地了解IO模型,我们需要先回顾下几个概念:同步、异步、阻塞、非阻塞

同步:

一个进程在执行某个任务时,另外一个进程必须等待其执行完毕,才能继续执行。就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。

异步:

一个进程在执行某个任务时,其他进程不必等待其执行完毕就能开始执行。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知。那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。

阻塞:

阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。

非阻塞:

不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。

总结:

  1. 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成;
  2. 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程。

二、阻塞IO

阻塞IO模型流程如下:

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

实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用recv(1024)的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。

 1 from socket import *
 2 server = socket(AF_INET,SOCK_STREAM)
 3 server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
 4 server.bind(('127.0.0.1',8080))
 5 server.listen(5)
 6 print('server start...')
 7 while True:
 8     conn,addr = server.accept()  #IO操作 在这accept的时候不能干recv的活
 9     print(addr)
10     while True:
11         try:
12             data = conn.recv(1024)  #IO操作
13             conn.send(data.upper())
14         except Exception:
15             break
16     conn.close()
17 server.close()
服务端
 1 from socket import *
 2 client  = socket(AF_INET,SOCK_STREAM)
 3 client.connect(('127.0.0.1',8080))
 4 while True:
 5     cmd = input('>>:').strip()
 6     if not cmd:continue
 7     client.send(cmd.encode('utf-8'))
 8     data = client.recv(1024)
 9     print('接受的是:%s'%data.decode('utf-8'))
10 client.close()
客户端

一旦阻塞了就在那卡着直到等到数据已经到了操作系统,操作系统再从内核拷贝给应用程序阻塞IO在那两个阶段全都阻塞住了。这样要想实现并发,可以使用多线程,多进程或线程池等方式。而多线程一旦连接过多,线程过多,会极大消耗系统资源。

三、非阻塞IO

recvfrom发起系统调用后, 如果数据未准备好, 内核也会立即返回, 这样用户程序可以不用阻塞继续执行后面的操作,但需要循环向内核发出系统调用来查询数据是否准备好,一旦数据准备好,recvfrom便会发起系统调用从内核空间拷贝数据,拷贝数据的过程是阻塞的。

 1 from socket import *
 2 
 3 server = socket(AF_INET, SOCK_STREAM)
 4 server.bind(('127.0.0.1',8099))
 5 server.listen(5)
 6 server.setblocking(False)
 7 
 8 
 9 rlist=[]
10 wlist=[]
11 while True:
12     try:
13         conn, addr = server.accept()
14         rlist.append(conn)
15         print(rlist)
16 
17     except BlockingIOError:
18         del_rlist=[]
19         for sock in rlist:
20             try:
21                 data=sock.recv(1024)
22                 if not data:
23                     del_rlist.append(sock)
24                 wlist.append((sock,data.upper()))
25             except BlockingIOError:
26                 continue
27             except Exception:
28                 sock.close()
29                 del_rlist.append(sock)
30 
31         del_wlist=[]
32         for item in wlist:
33             try:
34                 sock = item[0]
35                 data = item[1]
36                 sock.send(data)
37                 del_wlist.append(item)
38             except BlockingIOError:
39                 pass
40 
41         for item in del_wlist:
42             wlist.remove(item)
43 
44 
45         for sock in del_rlist:
46             rlist.remove(sock)
47 
48 server.close()
服务端
 1 from socket import *
 2 c=socket(AF_INET,SOCK_STREAM)
 3 c.connect(('127.0.0.1',8080))
 4 
 5 while True:
 6     msg=input('>>: ')
 7     if not msg:continue
 8     c.send(msg.encode('utf-8'))
 9     data=c.recv(1024)
10     print(data.decode('utf-8'))
客户端

这样也存在一个缺点:大量进行系统调用, 会极大消耗cpu资源; 同时由于查询间隔, 将不能及时的获取数据。

四、多路复用IO

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

   强调:

    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。

    结论: select的优势在于可以处理多个连接,不适用于单个连接

 1 from socket import *
 2 import select
 3 server = socket(AF_INET,SOCK_STREAM)
 4 server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
 5 server.bind(('127.0.0.1',8081))
 6 server.setblocking(False) #设置socket的套接字为非阻塞的
 7 server.listen(5)
 8 print('start running....')
 9 read_l = [server,]  #因为不只就那么一个列表要检测。所以不要在参数里面定死了
10 while True:
11     r_l,w_l,x_l = select.select(read_l,[],[])  #select()方法有四个参数
12     print(r_l)  #一开始服务端运行的时候,就等着,当你客户端一链接的时候,他就
13                 # 检测到有数据了(检测那个数据准备好了)
14     for obj in r_l:
15         if obj == server:
16             conn,addr = obj.accept()  #accept要经历两个阶段,但是程序如果走到这一步,那肯定是数据准备好了
17                          #当数据已经准备好的时候,accept就只经历一个copy数据的阶段了
18             # print(addr)
19             read_l.append(conn)  #在监听一下conn套接字(这时候已经监听了两个了:分别是accept,conn)
20         else:
21             data = obj.recv(1024)  # 此时的obj=conn
22             obj.send(data.upper())
23 #         obj.close()
24 # server.close()
服务端
 1 from socket import *
 2 import select
 3 client = socket(AF_INET,SOCK_STREAM)
 4 client.connect(('127.0.0.1',8081))
 5 while True:
 6     cmd = input('>>:')
 7     client.send(cmd.encode('utf-8'))
 8     data = client.recv(1024)
 9     print('接收的是:%s'%data.decode('utf-8'))
10 client.close()
客户端

优点:

相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。select模块用select方法检测那个套接字准备好了,也就是收没收到数据(而我们的非阻塞IO你不知道那个套接字准备好了,那么用select模块就能解决这个问题)。select还可以检测多个套接字,所以select比非阻塞IO的效率高。

缺点:

首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。
如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。

五、异步IO

未完待续……

猜你喜欢

转载自www.cnblogs.com/chuanxiaopang/p/10182273.html
今日推荐