zoukankan      html  css  js  c++  java
  • 第五十七篇 IO模型

    一、IO模型简介

    1.什么是IO模型

    1.模型:即解决某个问题的固定套路

    2.IO:输入输出

    3.IO模型:就是解决IO操作时需要等待的问题的模型

    2.IO的特点

    1.输入/输出数据所需要的时间对于CPU而言非常长

    2.IO操作所需要的等待时间,会让CPU处于闲置状态,造成资源浪费

    3.注意:IO有很多种类型,如socket网络IO、内存到内存的copy、键盘输入等等,其中socket网络IO需要等待的时间最长,也是我们学习的重点

    3.为什么需要IO模型

    可以在等待IO操作的过程中,利用CPU做别的事

    二、socket网络IO

    1.操作系统的两种状态:

    1.内核态(kernel):等待数据准备(例如,数据从网络到网卡,最后拷贝到系统内核的缓存中)

    2.用户态(process/thread):将数据从内核拷贝到进程中(操作系统接收完数据后,把数据从操作系统缓冲区中,copy到应用程序的缓冲区,操作系统从内核态转化为用户态)

    2.涉及的两个步骤

    1.wait_data(内核态)

    2.copy_data(用户态)

    3.例如,recv和accept需要经历wait_data --> copy_data;send只需要经历copy_data

    三、IO模型详解

    1.阻塞IO

    1.默认情况下,我们写的普通的TCP程序就是阻塞IO模型

    2.该模型的方式,就是当执行recv/accept 会进入wait_data的阶段,

    **3.而在这个阶段中,进程会主动调用一个block指令,进程进入阻塞状态,同时让出CPU的执行权,操作系统就会将CPU分配给其它的任务,从而提高了CPU的利用率 **

    **4.当recv/accept的数据到达时,首先会从内核将数据copy到应用程序缓冲区,并且socket将唤醒处于自身的等待队列中的所有进程(某个文件中有一个装着所有进程的列表,通过遍历列表来唤醒所有进程) **

    5.因此,之前使用多线程、多进程 完成的并发 其实都是阻塞IO模型 每个线程在执行recv时,都会卡住

    # 服务端
    from socket import *
    
    server = socket()
    server.bind(('127.0.0.1', 8000))
    server.listen()
    
    while True:
        client, address = server.accept()
        
        while True:
            msg = client.recv(1024)
            client.send(msg.upper())
    

    2.非阻塞IO

    **1.非阻塞IO模型与阻塞模型相反 ,在调用recv/accept 时都不会阻塞当前线程 **

    2.该模型在没有数据到达时,会跑出异常,我们需要捕获异常,然后继续不断地询问系统内核,直到数据到达为止

    3.该模型会大量的占用CPU资源做一些无效的循环, 效率低于阻塞IO

    4.使用方法: 将原本阻塞的socket 设置为非阻塞

    from socket import *
    import time
    
    s = socket()
    s.bind(('127.0.01', 8000))
    
    # 设置为非阻塞模式(socket中自带的方法)
    s.setblocking(False)  # 默认为True,所有要将状态改为False
    
    s.listen()
    
    # 保存所有的客户端
    cl = list()
    
    # 用于保存需要发送的数据
    msgl = list()
    
    while True:
        try:
            c,addr = s.accept()  # 完成三次握手
            cl.append(c)
    	except BlockingIOError:
    	
    		# 代码执行到这里说明,没有连接需要处理
    		# 则可以利用CPU处理收发数据的任务
    		for c in cl[:]:    # 只处理接收数据
    			try:
    				data = c.recv(1024)
    				if not data: 
    					raise ConnectionResetError
    				msgl.append((c, data.upper()))  # 将客户端和接收的数据打包
    				# c.send(data.upper()) # 不能直接发送,因为当内核缓冲区满了,就会抛出异常
    			except BlockingIOError:
    				pass  # 如果遇到阻塞则说明,数据客户端没再发数据
    			except ConnectionResetError: 
    				# 如果遇到这个异常,说明客户端自动关闭了
    				# 所有服务器也需要关闭连接,并删除这个客户端
    				c.close()
    				cl.remove(c)
    		
    		# 只处理发送数据
    		for i in msgl[:]:
    			try:
    				i[0].send(i[1])
    				msgl.remove(i)   # 一个客户端发送完它的信息则移除
    			except BlockingIOError:
    				pass
    			except ConnectionResetError:
    				# 关闭连接
    				i[0].close()
    				# 删除数据
    				msgl.remove(i)
    				# 删除链接
    				cl.remove(i[0])
    

    3.多路复用IO

    **1.属于事件驱动模型 **

    2.多个socket使用同一套处理逻辑

    1.如果将非阻塞IO,比喻是点餐的话,相当于你每次去前台,照着菜单挨个问个遍,循环往复,效率很低
    
    2.多路复用,直接问前台哪些菜做好了,前台会给你返回一个列表,里面就是已经做好的菜 
    
    3.多路复用对比非阻塞 ,多路复用可以极大降低CPU的占用率  
    

    3.对比阻塞或非阻塞模型,增加了一个select,来帮我们检测socket的状态,从而避免了我们自己检测socket带来的开销

    4.select会把已经就绪的放入列表中,我们需要遍历列表,分别处理读写即可

    5.注意:多路复用并不完美 因为本质上多个任务之间是串行的,如果某个任务耗时较长将导致其他的任务不能立即执行,多路复用最大的优势就是高并发

    import socket
    import time
    import select
    
    s = socket.socket()
    s.bind(('127.0.0.1', 8000))
    
    # 在多路复用中,阻塞与非阻塞没有区别,因为select函数会阻塞到有数据到达为止
    s.setblocking(True)
    
    s.listen()
    
    # 待检测是否可读
    read_list = [s]  # 将服务端放入列表,发给select去判断是否有数据
    
    # 待检测是否可写
    write_list = []   # select会返回可写列表,用于哪些客户端需要回应
    
    # 待发送的数据,里面是客户端和发给该客户端的内容
    msgs = {}
    
    print('start...')
    while True:
    
    	# 通过select中的select方法返回可读的对象和可写的对象(也就是服务端和客户端都可以读或写)
        read_ables, write_ables, _ = select.select(read_list, write_list, [])
        
        # 如果服务端在可读的对象中,说明有数据传输(IO操作)
        for obj in read_ables:   # 拿出所有可以读数据的socket对象(有可能是服务器,也有可能是客户端)
            if s == obj:   # 如果对象是服务端,说明有客户端连接进来了
                client, addr = s.accept()  # 获取客户端对象和地址
                read_list.append(client)  # 将与服务端连接的客户端添加到可读列表中
                
    		else:   # 如果是客户端对象可读,说明服务端需要利用客户端对象接收数据了
    			try:
    				data = obj.recv(1024)
    				if not data: raise ConnectionResetError
    				print(data)
    				
    				# 将发送信息的客户端添加到可写列表中
    				write_list.append(obj)
    				if obj in msgs:  # 如果信息字典中有此客户端对象
    					msgs[obj].append(data)  # 则只需要将它发送的信息添加到信息字典中所对应的列表
    				else:
    					msgs[obj] = [data]  # 否则要创建一个键值对,用于表示客户端对象所对应它发送的信息
    			except ConnectionResetError:
    				obj.close()  # 当断开连接时,关闭客户端
    				read_list.remove(obj)  # 并移除客户端
    	
    	# 处理可写内容,也即是服务端回复客户端
    	for obj in write_ables:  # 取出所有的可写对象
    		msg_list = msgs.get(obj)  # 在信息字典中取出要回复的信息列表
    		if msg_list: # 如果有信息列表,则发送
    			for m in msg_list: # 取出该客户端发送的所有的信息
    				try:
    					obj.send(m.upper())  # 通过客户端对象发送信息
    				except ConnectionResetError:
    					obj.close()  # 当客户端断开连接时,服务端也断开连接
    					write_list.remove(obj)  # 当客户端退出之后,移除该客户端对象
    			msgs.pop(obj) # 发送完该对象的信息,就从信息字典中删除该客户端及其信息
    		
    		write_list.remover(obj) # 发送完一个对象的信息就将它删除
    

    4.异步IO

    非阻塞IO不等于异步IO,,因为非阻塞IO中的copy的过程是一个同步任务,会卡住当前线程,而异步IO,是发起任务后 就可以继续执行其它任务,当数据copy到应用程序缓冲区,才会给你的线程发送信号 或者执行回调

    asyncio

    5.信号驱动IO

    简单的说就是,当某个事情发生后,会给你的线程发送一个信号,你的线程就可以去处理这个任务

    不常用,原因是 socket的信号太多,处理起来非常繁琐

    6.不要在迭代的过程中操作元素

    # a = [1,2,3,4,5]
    # for i in a[:]:  # [:] 会复制一份一模一样的列表
    #     print(i)
    #     if i == 2:
    #         a.remove(i)
    #     #     a.append(10)
    # print(a)
    
    # a = {"a":1,"b":2}
    # for k in a:
    #     if k == "a":
    #         a.pop(k)
    
    a = [(1,2),(3,2),(100,21),(31,22),(33,22)]
    
    index = 0
    for i in a[:]:
        print(i)
        if index == 1:
            a.remove(i)
        index += 1
    print(a)
    
  • 相关阅读:
    三、linux系统管理
    二、基本命令
    一、基本环境
    mysql-day4
    mysql-day3
    mysql-day2
    mysql-day1
    3、线性表的链式存储结构
    2、线性表之顺序表
    1、时间复杂度
  • 原文地址:https://www.cnblogs.com/itboy-newking/p/11185363.html
Copyright © 2011-2022 走看看