IO模型

#1. 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。

#2. 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程

 

同步

#所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。
#举例:
#1. multiprocessing.Pool下的apply #发起同步调用后,就在原地等着任务结束,根本不考虑任务是在计算还是在io阻塞,总之就是一股脑地等任务结束
#2. concurrent.futures.ProcessPoolExecutor().submit(func,).result()
#3. concurrent.futures.ThreadPoolExecutor().submit(func,).result()

 

异步

#异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
#举例:
#1. multiprocessing.Pool().apply_async() #发起异步调用后,并不会等待任务结束才返回,相反,会立即获取一个临时结果(并不是最终的结果,可能是封装好的一个对象)。
#2. concurrent.futures.ProcessPoolExecutor(3).submit(func,)
#3. concurrent.futures.ThreadPoolExecutor(3).submit(func,)

 

阻塞

#阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
#举例:
#1. 同步调用:apply一个累计1亿次的任务,该调用会一直等待,直到任务返回结果为止,但并未阻塞住(即便是被抢走cpu的执行权限,那也是处于就绪态);
#2. 阻塞调用:当socket工作在阻塞模式的时候,如果没有数据的情况下调用recv函数,则当前线程就会被挂起,直到有数据为止。

 

非阻塞

#非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。

 

 

 

 

 一、IO发生时涉及的对象和步骤

    对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个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 Model:

   阻塞IO          (blocking IO)

   非阻塞IO      (nonblocking IO)

   IO多路复用        ( IO multiplexing)

   异步IO       (signal driven IO)

   信号驱动(了解)   (asynchronous IO)


    
   

 

阻塞IO

socket模块默认就是阻塞的
问题:同一时间只能服务一个客户端

方法1:多线程
    优点:如果并发量不高 效率是较高的 因为每一个客户端都有单独线程来处理
    弊端:不可能无限的开启线程 线程也需要占用资源

方式2:多进程

    优点: 可以多个CPU并行处理
    弊端: 占用资源非常大,一旦客户端稍微多一点 立马就变慢了

线程池:
    优点: 保证了服务器正常稳定运行,还帮你负责创建和销毁线程,以及任务分配
    弊端: 一旦并发量超出最大线程数量,就只能等前面的运行完毕

进程池:

真正导致效率低的是阻塞问题  但是上述几个方法 并没有真正解决阻塞问题 仅仅是避开了阻塞问题

 

非阻塞IO

  setblocking(False)

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

import socket
import time
s = socket.socket()
s.bind(("127.0.0.1",9999))
s.listen()
# 设置socket 是否阻塞 默认为True
s.setblocking(False)

# 所有的客户端socket
cs = []
# 所有需要返回数据的客户端
send_cs = []

while True:
    # time.sleep(0.2)
    try:
        c,addr = s.accept() # 三次握手
        print("run accept")
        cs.append(c) #存储已经连接成功的客户端
    except BlockingIOError:
        # 没有数据准备 可以作别的事情
        # print("收数据")
        for c in cs[:]:
            try:
                data = c.recv(1024)
                if not data:
                    c.close()
                    cs.remove(c)
                print(data.decode("utf-8"))
                # 把数据和连接放进去
                send_cs.append((c, data))
                #c.send(data.upper()) # io
                # send也是io操作  在一些极端情况下 例如系统缓存满了 放不进去 那肯定抛出
                # 非阻塞异常  这时候必须把发送数据 单独拿出来处理 因为recv和send都有可能抛出相同异常
                # 就无法判断如何处理


            except BlockingIOError:
                continue

            except ConnectionResetError:
                c.close()
                # 从所有客户端列表中删除这个连接
                cs.remove(c)

        # print("发数据")
        for item in send_cs[:]:
            c,data = item
            try:
                c.send(data.upper())
                # 如果发送成功就把数据从列表中删除
                send_cs.remove(item)
            except BlockingIOError: # 如果缓冲区慢了 那就下次再发
                continue
            except ConnectionResetError:
                c.close() # 关闭连接
                send_cs.remove(item) # 删除数据
                # 从所有客户端中删除这个已经断开的连接
                cs.remove(c)
非阻塞IO、套接字服务端

 补充:

li = [1,2,3,4,5,6]

for i in li[:]:                     #
    li.remove(i)
print(li)
迭代取值不修改元素

 

 但是非阻塞IO模型绝不被推荐

其优点:

#能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在“”同时“”执行)

其缺点
#1. 无限循环调用recv(),CPU占用率太高;
#2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

 

 

多路复用IO

    IO多路复用
    用一个线程来并发处理所有的客户端

    原本我们是直接向操作系统 要数据,
        如果是阻塞IO  没有数据就进入阻塞状态
        非阻塞IO   没有数据就抛出异常 然后继续询问操作系统

    在多路复用模型中,筛选出已经准备就绪的socket,会将可读和科协的分别放到不同的列表中
    既然是已经就绪 那么执行recv或是send 就不会在阻塞

    select模块只有一个函数就是select
    参数1:r_list 需要被select检测是否是可读的客户端  把所有socket放到该列表中,select会负责从中找出可以读取数据的socket
    参数2:w_lirt 需要被select检测是否是可写的客户端  把所有socket放到该列表中,select会负责从中找出可以写入数据的socket
    参数3:x_list 存储要检测异常条件 ....忽略即可

    返回一个元组 包含三个列表
        readables 已经处于可读状态的socket  即数据已经到达缓冲区
        writeables 已经处于可写状态的socket  即缓冲区没满 可以发送...
        x_list:忽略

    从可读或写列表中拿出所有的socket 依次处理它们即可
import socket
import time
import select
s = socket.socket()
s.bind(("127.0.0.1",9999))
s.listen()
# 在多路复用中 一旦select交给你一个socket 一定意味着 该socket已经准备就绪 可读或是可写
# s.setblocking(False)

r_list = [s]
w_list = []
# 存储需要发送的数据 已及对应的socket  把socket作为key 数据作为value
data_dic = {}

while True:
    readables,writeables,_ = select.select(r_list,w_list,[])

    # 接收数据 以及服务器建立连接
    for i in readables:
        if i == s:# 如果是服务器  就执行accept
            c,_ = i.accept()
            r_list.append(c)
        else: # 是一个客户端端 那就recv收数据
            try:
                data = i.recv(1024)
                if not data: #linux 对方强行下线或是 windows正常下线
                    i.close()
                    r_list.remove(i)
                    continue
                print(data)
                # 发送数据 不清楚 目前是不是可以发 所以交给select来检测
                w_list.append(i)
                data_dic[i] = data # 把要发送的数据先存在 等select告诉你这个连接可以发送时再发送
            except ConnectionResetError:# windows强行下线
                i.close()
                r_list.remove(i) # 从检测列表中删除

    # 发送数据
    for i in writeables:
        try:
            i.send(data_dic[i].upper()) # 返回数据
            #data_dic.pop(i)
            #w_list.remove(i)
        except ConnectionResetError:
            i.close()
        finally:
            data_dic.pop(i) # 删除已经发送成功的数
            w_list.remove(i) # 从检测列表中删除这个连接  如果不删除 将一直处于可写状态
IO多路复用,套接字服务端

 

 select监听fd变化的过程分析:

#用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后,就会发送信号给用户进程数据已到;
#用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除,这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。

 

该模型的优点:

#相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

 

该模型的缺点:

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

 

转载于:https://www.cnblogs.com/pdun/p/10516467.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值