并发编程-IO模型
Posted yangyuanhu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程-IO模型相关的知识,希望对你有一定的参考价值。
IO模型
模型即解决某个问题的固定套路
I/O 指的是输入输出
IO的问题: 当我们要输入数据或是输出数据通常需要很长一段时间,当然是对于CPU而言
在等待输入的过程中,CPU就处于闲置状态 没事干! 造成了资源浪费
注意: IO其实有很多类型,例如,socket网络IO,内存到内存的copy,等待键盘输入,对比起来socket网络IO需要等待的时间是最长的,这也是咱们重点关注的地方,
学习IO模型要干什么? 就是在等待IO操作的过程中利用CPU,做别的事情
网络IO经历的步骤和过程
操作系统有两种状态:内核态 和 用户态 , 当操作系统需要控制硬件时,例如接收网卡上的数据,必须先转换到内核态,接收完数据后,要把数据从操作系统缓冲区,copy到应用程序的缓冲区,从内核态转为用户态;
涉及到的步骤
1.wait_data
2.copy_data
recv accept 需要经历 wait -> copy
send 只需要经历copy
阻塞IO模型
默认情况下 你写出TCP程序就是阻塞IO模型
该模型 提高效率方式,当你执行recv/accept 会进入wait_data的阶段,
1.你的进程会主动调用一个block指令,进程进入阻塞状态,同时让出CPU的执行权,操作系统就会将CPU分配给其它的任务,从而提高了CPU的利用率
2.当数据到达时,首先会从内核将数据copy到应用程序缓冲区,并且socket将唤醒处于自身的等待队列中的所有进程
注意:之前使用多线程 多进程 完成的并发 其实都是阻塞IO模型 每个线程在执行recv时,也会卡住
非阻塞IO模型
非阻塞IO模型与阻塞模型相反 ,在调用recv/accept 时都不会阻塞当前线程
使用方法: 将原本阻塞的socket 设置为非阻塞
该模型在没有数据到达时,会抛出异常,我们需要捕获异常,然后继续不断地询问系统内核直到数据到达为止
案例:
import socket
import time
s = socket.socket()
s.bind(("127.0.0.1",1688))
# 设置为非阻塞 模型
s.setblocking(False) #False表示不阻塞
s.listen()
# 保存所有的客户端socket
cs = []
# 用于保存需要发送的数据
msgs = []
while True:
try:
c,addr = s.accept() # 完成三次握手
print("来客了, xxx接客了!")
cs.append(c)
except BlockingIOError:
print("还没有人来/. 好寂寞啊!")
# 代码执行到这里则说明 没有连接需要处理
# 就可以处理收发数据的任务
for c in cs[:]: # 专门处理接收数据
try:
data = c.recv(1024)
if not data:
raise ConnectionResetError
msgs.append((c,data.upper()))
#c.send(data.upper()) # 不能直接发 当内核缓冲区满了 就会抛出异常 导致数据发不出去了
except BlockingIOError:
print("客官 你倒是说句话啊!")
pass
except ConnectionResetError:
c.close()
cs.remove(c)
# 处理发送数据
for i in msgs[:]:
try:
i[0].send(i[1])
msgs.remove(i)
except BlockingIOError:
pass
except ConnectionResetError:
#关闭连接
i[0].close()
# 删除数据
msgs.remove(i)
# 删除连接
cs.remove(i[0])
客户端
import socket
c = socket.socket()
c.connect(("127.0.0.1",1688))
while True:
msg = input("").strip()
if not msg:continue
c.send(msg.encode("utf-8"))
print(c.recv(1024).decode('utf-8'))
可以看出,该模型会大量的占用CPU资源做一些无效的循环, 效率低于阻塞IO
多路复用IO模型
属于事件驱动模型
多个socket使用同一套处理逻辑
如果将非阻塞IO 比喻是点餐的话,相当于你每次去前台,照着菜单挨个问个遍
而多路复用,相当于直接问前台那些菜做好了,前台会给你返回一个列表,里面就是已经做好的菜
对比阻塞或非阻塞模型,增加了一个select,来帮我们检测socket的状态,从而避免了我们自己检测socket带来的开销
select会把已经就绪的socket放入列表中,我们需要遍历列表,分别处理读写即可
多路复用中select的职责:
案例:
import socket
import time
import select
s = socket.socket()
s.bind(("127.0.0.1",1688))
# 设置为非阻塞 模型
s.setblocking(True) #在多路复用中 阻塞与非阻塞没有区别 因为select会阻塞直到有数据到达为止
s.listen()
# 待检测是否可读的列表
r_list = [s]
# 待检测是否可写的列表
w_list = []
# 待发送的数据
msgs =
print("开始检测了")
while True:
read_ables, write_ables, _= select.select(r_list,w_list,[])
print("检测出结果了!")
# print(read_ables,"可以收数据了")
# print(write_ables,"可以发数据了")
# 处理可读 也就是接收数据的
for obj in read_ables: # 拿出所有可以读数据的socket
#有可能是服务器 有可能是客户端
if s == obj: # 服务器
print("来了一个客户端 要连接")
client,addr = s.accept()
r_list.append(client) # 新的客户端也交给select检测了
else:# 如果是客户端则执行recv 接收数据
print("客户端发来一个数据")
try:
data = obj.recv(1024)
if not data:raise ConnectionResetError
print("有个客户端说:",data)
# 将要发送数据的socket加入到列表中让select检测
w_list.append(obj)
# 将要发送的数据已经socket对象丢到容器中
if obj in msgs: # 由于容器是一个列表 所以需要先判断是否已经存在了列表
msgs[obj].append(data)
else:
msgs[obj] = [data]
except ConnectionResetError:
obj.close()
r_list.remove(obj)
# 处理可写的 也就是send发送数据
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()
w_list.remove(obj)
break
# 数据从容器中删除
msgs.pop(obj)
# 将这个socket从w_list中删除
w_list.remove(obj)
客户端
import socket
c = socket.socket()
c.connect(("127.0.0.1",1688))
while True:
msg = input("").strip()
if not msg:continue
c.send(msg.encode("utf-8"))
print(c.recv(1024).decode('utf-8'))
多路复用对比非阻塞 ,多路复用可以极大降低CPU的占用率
注意:多路复用并不完美 因为本质上多个任务之间是串行的,如果某个任务耗时较长将导致其他的任务不能立即执行,多路复用最大的优势就是高并发
另外select 最大仅支持1024的socket同时处理,所以linux下的epoll才是最好的多路复用模型,详见:
https://www.cnblogs.com/yangyuanhu/p/11152914.html
异步IO模型
非阻塞IO不等于异步IO 因为copy的过程是一个同步任务 会卡主当前线程
而异步IO 是发起任务后 就可以继续执行其它任务,当数据copy到应用程序缓冲区完成后,才会给你的线程发送信号 或者执行回调
asyncio python3.4 出现 目前很多重要的第三方模块都不支持该模块,所以没有广泛使用,其内部也是使用了生成器!
信号驱动IO模型
简单的说就是 当某个事情发生后 会给你的线程发送一个信号,你的线程就可以去处理这个任务
不常用,原因是 socket的信号太多,处理起来非常繁琐
以上是关于并发编程-IO模型的主要内容,如果未能解决你的问题,请参考以下文章