1. I/O模型介绍

1.1 I/O模型基础

更好的理解I/O模型,需要先回顾:同步、异步、阻塞、非阻塞

  • 同步:执行完代码后,原地等待,直至出现结果
  • 异步:执行完代码后,不等待,继续执行其他事务(常与回调机制关联)
  • 阻塞:cpu在遇到I/O操作,进入阻塞状态,cpu切换到其他任务
  • 非阻塞:不会遇到I/O操作,cpu一直处于计算状态

注:同步不等于阻塞

I/O模型总计有五种,其中信号驱动I/O,在实际中并不常用,主要还是学习另外四种I/O模型

  • 阻塞I/O模型(blocking IO)
  • 非阻塞I/O模型(nonblocking IO)
  • 多路复用I/O模型(IO multiplexing)
  • 异步I/O模型(asynchronous IO)

web开发中主要碰到的是网络I/O,对于一个network IO 它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,该操作会经历两个阶段:

#1)等待数据准备 (Waiting for the data to be ready)
#2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

这些IO模型的区别就是在两个阶段上各有不同的情况。在网络中常用的I/O操作有(accept,recv,send),其中send的感官比较少,主要是只存在本地copy阶段,对于网络传输如何不关注。

1.2 阻塞I/O模型

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

python基础-并发编程之I/O模型基础

    当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于网络 io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。

    而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,阻塞IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。

    几乎所有的程序员第一次接触到的网络编程都是从listen()、send()、recv() 等接口开始的,使用这些接口可以很方便的构建服务器/客户机的模型。然而大部分的socket接口都是阻塞型的。如下图

    ps:所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。

python基础-并发编程之I/O模型基础

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

    一个简单的解决方案:

#在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。

  该方案的问题是:

#开启多进程或都线程的方式,在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。

改进方案:

#很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。

改进后方案其实也存在着问题:

#“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

例子

from socket import *
from threading import Thread

def communicate(conn):
    while True:
        try:
            data = conn.recv(1024)
            if not data: break
            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()



server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)

while True:
    print('starting...')
    conn, addr = server.accept()
    print(addr)

    t=Thread(target=communicate,args=(conn,))
    t.start()

server.close()
服务端

相关文章: