并发编程——IO模型(6)

1.IO模型分类

  • 同步IO
  • #所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。
    #举例:
    #1. multiprocessing.Pool下的apply #发起同步调用后,就在原地等着任务结束,根本不考虑任务是在计算还是在io阻塞,总之就是一股脑地等任务结束
    #2. concurrent.futures.ProcessPoolExecutor().submit(func,).result()
    #3. concurrent.futures.ThreadPoolExecutor().submit(func,).result()
  • 异步IO
  • #异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
    #举例:
    #1. multiprocessing.Pool().apply_async() #发起异步调用后,并不会等待任务结束才返回,相反,会立即获取一个临时结果(并不是最终的结果,可能是封装好的一个对象)。
    #2. concurrent.futures.ProcessPoolExecutor(3).submit(func,)
    #3. concurrent.futures.ThreadPoolExecutor(3).submit(func,)
  • 阻塞IO
  • #阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
    #举例:
    #1. 同步调用:apply一个累计1亿次的任务,该调用会一直等待,直到任务返回结果为止,但并未阻塞住(即便是被抢走cpu的执行权限,那也是处于就绪态);
    #2. 阻塞调用:当socket工作在阻塞模式的时候,如果没有数据的情况下调用recv函数,则当前线程就会被挂起,直到有数据为止。
  • 非阻塞IO
  • #非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。
  • 小结
  • #1. 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。
    
    #2. 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程

     

2.阻塞IO

  • blocking IO 的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了
  • 解决方案:在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程)
  • 上述方案存在的问题:开启多进程或多线程的方式,在遇到要同时响应成百上千的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态
  • 改进方案:用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务;“连接池”维持连接的缓存池,尽量重用已有的连接,减少创建和关闭连接的频率。这两种方式都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等
  • 改进方案存在的问题:“线程池”和“连接池”技术上也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”,终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以,考虑“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
  • 总结:多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。

3.非阻塞IO

  • #-*- coding:utf-8 -*-
    from socket import *
    ip = gethostbyname(gethostname())
    server = socket(AF_INET,SOCK_STREAM)
    server.bind((ip,8080))
    server.listen(5)
    server.setblocking(False)#非阻塞IO
    
    rlist = []
    wlist = []
    while True:
        try:
            conn,addr = server.accept()
            rlist.append(conn)
            print(rlist)
        except BlockingIOError:
            del_rlist = []
            for sock in rlist:
                try:
                    data = sock.recv(1024)
                    if not data:
                        del_rlist.append(sock)
                    else:
                        wlist.append((sock,data.upper()))
                except BlockingIOError:
                    continue
                except Exception:
                    sock.close()
                    del_rlist.append(sock)
            del_wlist = []
            for item in wlist:
                try:
                    sock = item[0]
                    data = item[1]
                    sock.send(data)
                    del_wlist.append(item)
                except BlockingIOError:
                    pass
            for item in del_wlist:
                wlist.remove(item)
            for sock in del_rlist:
                rlist.remove(sock)
    server.close()
    服务端
  •  1 #-*- coding:utf-8 -*-
     2 import socket
     3 
     4 server_ip = socket.gethostbyname(socket.gethostname())
     5 client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
     6 client.connect((server_ip,8080))
     7 while True:
     8     msg = input('>>: ')
     9     if not msg: continue
    10     client.send(msg.encode('utf-8'))
    11     data = client.recv(1024)
    12     print(data.decode('utf-8'))
    客户端

     

  • 优点:能够在等待任务完成的时间里干其他的活(包括提交其他任务,也就是“后台”可以有多个任务在“同时”进行)
  • 缺点:
    • 1.循环调用recv()将大幅度推高CPU占用率,这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况
    • 2.任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能再两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低  
  • 解决方案:select()多路复用模式,可以一次检测多个连接是否活跃

4.多路复用IO

  • select/epoll的好处在于单个process就可以同时处理多个网络连接的IO。select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
  • 强调
    • 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
    • 3.结论: select的优势在于可以处理多个连接,不适用于单个连接
  •  1 # -*- coding:utf-8 -*-
     2 from socket import *
     3 import select
     4 
     5 ip = gethostbyname(gethostname())
     6 server = socket(AF_INET, SOCK_STREAM)
     7 server.bind((ip, 8080))
     8 server.listen(5)
     9 server.setblocking(False)  # 非阻塞IO
    10 print("starting...")
    11 
    12 rlist = [server, ]
    13 wlist = []
    14 wdata = {}
    15 
    16 while True:
    17     rl, wl, xl = select.select(rlist, wlist, [], 0.5)
    18     print(wl)
    19     for sock in rl:
    20         if sock == server:
    21             conn,addr = sock.accept()
    22             rlist.append(conn)
    23         else:
    24             try:
    25                 data = sock.recv(1024)
    26                 if not data:
    27                     sock.close()
    28                     rlist.remove(sock)
    29                     continue
    30                 wlist.append(sock)
    31                 wdata[sock]=data.upper()
    32             except Exception:
    33                 sock.close()
    34                 rlist.remove(sock)
    35     for sock in wl:
    36         sock.send(wdata[sock])
    37         wlist.remove(sock)
    38         wdata.pop(sock)
    服务端

     

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

 

posted @ 2018-03-10 16:00  GraceZen  阅读(180)  评论(0编辑  收藏  举报