第五十七篇 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)
原文地址:https://www.cnblogs.com/itboy-newking/p/11185363.html