并发编程(三) IO模型

五 IO模型

常用的IO模型有4种:

  • 阻塞IO
  • 非阻塞IO
  • IO多路复用
  • 异步IO

不常用的有:

  • 驱动信号

5.1 阻塞IO、非阻塞IO

  • 阻塞IO:进程不能做其他的事情
  • 非阻塞IO:等待数据无阻塞

阻塞IO

阻塞IO就是全程阻塞,其中,全程指的是等待数据和 数据从内核态拷贝到用户态。

全程阻塞就是以上两个步骤都阻塞。如图:

系统调用两个阶段:

  • wait for data:阻塞
  • copy data:阻塞

非阻塞IO

非阻塞IO是部分阻塞,
等待数据时不会阻塞,而是在固定时间内循环发起系统调用,请求不到做自己的事情,等待下次请求,
而数据从内核态拷贝到用户态还是阻塞的。如图:

系统调用两个阶段:

  • wait for data:非阻塞
  • copy data:阻塞

优点:
等待数据无阻塞

缺点:
1.系统调用发送太多
2.数据不是即时接收的

ps:socket设置socket对象.setblocking(False) 设置阻塞状态为非阻塞

5.2 IO多路复用

IO多路复用:全程阻塞,监听多个链接

系统调用两个阶段:

  • wait for data:阻塞
  • copy data:阻塞

实现IO多路复用的常用方式有:

  • select
  • poll
  • epoll

原理

基本原理:
通过select/poll/epoll函数不断轮询所负责的所有socket套接字,当某个socket套接字有数据到达,就通知用户进程。

特点:
就是单个process可以同时处理多个网络连接的IO,

ps:不同的操作系统提供的函数不同:
windows系统: select
linux系统: select、poll、epoll

select模块

系统调用通过select模块完成wait for data的工作

示例:
select监听多个socket对象(sock是socket对象),实现并发

r, w, e = select.select([sock,], [], [])  # 等待链接 
for obj in r:
	conn, addr = obj.accept()

示例升级:

inputs = [sock,] 
r, w, e = select.select(inputs, [], [])  # inputs监听有变化的套接字 inputs=[sock,conn1,conn2,...]
for obj in r:  # 第一次[sock,] 第二次[conn1,]
	if obj == sock:  # 如果返回的r = sock,说明有连接请求
		conn, addr = obj.accept()
		inputs.append(conn)  # inputs=[sock, conn1, conn2]
	else:  # 否则,可以接收数据了
		data = obj.recv(1024)

ps:关于文件描述符的tips(socket套接字)

1.每一个套接字对象的本质就是一个非零整数,不会变(fb=4)

<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, 
proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 51963)>

2.收发数据的时候,对于接收端而言,数据先到内核空间,然后copy到用户空间,同时内核空间的数据被清空

3.根据TCP协议,当发送端接收到接收端的确认信息后,清空内核空间的数据,否则不清空

select、poll、epoll

select:

  • 每次调用select都要将所有的fd(文件描述符),copy到你的内核空间
  • 遍历所有的fd,是否有数据访问
  • 最大连接数(1024),超出链接不再监听

ps:select的特点也是其缺点,会导致效率下降:

poll:

  • 每次调用select都要将所有的fd(文件描述符),copy到你的内核空间
  • 遍历所有的fd,是否有数据访问
  • 最大连接数没有限制

epoll:

  • 不同于select和poll只有一个函数,epoll通过三个函数实现实现轮询socket:
    • 第一个函数:创建epoll句柄:将所有的fd(文件描述符),copy到你的内核空间,只copy一次
    • 回调函数:为所有fd绑定一个回调函数,一旦有数据访问,触发回调函数,回调函数将fd放入一个链表中
    • 第三个函数:判断链表是否为空
  • epoll最大连接数没有上线

ps:回调函数
某一个函数或者某一个动作,成功完成之后,会触发的函数

selectors模块

selectors是select的升级版

selectors基于select模块实现IO多路复用,调用语句selectors.DefaultSelector()创建selecters对象,特点是根据平台自动选择最佳IO多路复用机制,调用顺序:epoll > poll > select

import selectors
import socket

sel = selectors.DefaultSelector()  # 根据平台自动选择最佳IO多路复用机制

def accept(sock, mask):
    conn, addr = sock.accept()
    sel.register(conn, selectors.EVENT_READ, read)  # 将conn和read()注册到一起,当conn有变化时执行read()

def read(conn, mask):
    try:
        data = conn.recv(1000)
        print(data.decode('utf8'))
        inputs = input('>>:').strip()
        conn.send(inputs.encode('utf8'))
    except Exception:
        sel.unregister(conn)
        conn.close()

sock = socket.socket()
sock.bind(('127.0.0.1', 8080))
sock.listen(100)
sock.setblocking(False)  # 设置为非阻塞IO

sel.register(sock, selectors.EVENT_READ, accept)  # 将sock和accept()注册到一起,当sock有变化时执行accept()

while True:
    events = sel.select()  # 监听  [(key1,mask1),(key2),(mask2)]
    for key, mask in events:
        func = key.data  # 1 key.data就是accept   # 2 key.data就是read
        obj = key.fileobj  # 1 key.fileobj就是sock   # 2 key.fileobj就是conn

        func(obj, mask)  # 1 accept(sock,mask)   # 2read(conn,mask)
    

5.3 同步IO、异步IO

同步IO

只要系统调用中存在阻塞就是同步IO,
所以,阻塞IO、非阻塞IO、IO多路复用都是同步IO

异步IO

全程无阻塞,实现复杂

系统调用两个阶段:

  • wait for data:非阻塞
  • copy data:非阻塞
原文地址:https://www.cnblogs.com/sunqim16/p/6835896.html