引言
Python的I/O模型分为同步(sync)和异步(async)两种:
- 同步I/O模型是指,当一个线程在等待I/O操作完成时,它不能执行其他任务,需要一直等待I/O操作完成,直到接收到I/O操作的完成通知后才继续执行。
- 异步I/O模型是指,当一个线程发起一个I/O操作后,不会等待I/O操作完成,而是直接执行其他任务,当I/O操作完成后,再通过回调或事件通知来处理I/O操作的结果。
如果再对IO模型细分可以分为五种模型:
- 同步阻塞IO(Blocking IO):即传统的IO模型。
- 同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。
- IO多路复用(IO MulTIplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。
- 异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。
- signal driven IO:信号驱动IO , 在实际中并不常用,所以只剩下四种IO Model。
IO 模型详解
1、同步阻塞IO(Blocking IO)
Python 中的同步阻塞 I/O 是一种 I/O 操作,在这种情况下,程序的执行会被阻塞,直到 I/O 操作完成。换句话说,程序将等待 I/O 操作完成,才能继续执行下一个任务。这种类型的 I/O 被称为“阻塞”,因为它阻塞了程序的执行,并且被称为“同步”,因为它以同步的方式发生,程序等待 I/O 操作完成后再继续。
例如,当使用 Python 中的 read 方法从文件读取时,程序将等待整个文件读取完毕,才能继续执行下一个任务。这是同步阻塞 I/O 的一个例子。
当 I/O 操作相对较短且程序可以等待其完成后才继续执行下一个任务时,通常会使用同步阻塞 I/O。然而,当 I/O 操作比较长时,程序可能会被阻塞很长一段时间,从而导致性能下降。在这种情况下,通常更好使用异步 I/O。
在linux
中,默认情况下所有的socket都是阻塞IO,一个典型的读操作流程大概是这样:
其实还是没有解决IO问题 该等的地方还是得等 没有规避 只不过多个人等待彼此互不干扰。示例如下:
服务端:
import socket
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn, addr = server.accept()
while True:
try:
data = conn.recv(1024)
if len(data) == 0:break
print(data)
conn.send(data.upper())
except ConnectionResetError as e:
break
conn.close()
客户端:
import socket
client = socket.socket()
client.connect(('127.0.0.1',8081))
while True:
client.send(b'hello world')
data = client.recv(1024)
print(data)
2、同步非阻塞IO(Non-blocking IO)
Python 中的同步非阻塞 I/O 是一种程序不必等待 I/O 操作完成就可以继续执行下一个任务的 I/O 操作。相反,程序在 I/O 操作正在进行时继续执行下一个任务。这种类型的 I/O 被称为“非阻塞”,因为它不会阻塞程序的执行,并且被称为“同步”,因为它仍以同步的方式操作,程序会定期检查 I/O 操作的状态。
例如,在 Python 中实现同步非阻塞 I/O,可以使用 select 模块,该模块提供了对操作系统底层 I/O 多路复用功能的访问,允许您同时监视多个 I/O 操作,而不会阻塞。
当 I/O 操作预计需要很长时间才能完成,且程序需要在此期间继续处理其他任务时,通常使用同步非阻塞 I/O。这可以提高程序的性能和响应性。
python下,可以通过设置socket使其变为non-blocking(server.setblocking(False)
)。当对一个non-blocking socket执行读操作时,流程是这个样子:
示例如下:
服务端:
import socket
import time
server = socket.socket()
server.bind(('127.0.0.1', 8081))
server.listen(5)
server.setblocking(False)
# 将所有的网络阻塞变为非阻塞
r_list = []
del_list = []
while True:
try:
conn, addr = server.accept()
r_list.append(conn)
except BlockingIOError:
for conn in r_list:
try:
data = conn.recv(1024) # 没有消息 报错
if len(data) == 0: # 客户端断开链接
conn.close() # 关闭conn
# 将无用的conn从r_list删除
del_list.append(conn)
continue
conn.send(data.upper())
except BlockingIOError:
continue
except ConnectionResetError:
conn.close()
del_list.append(conn)
# 挥手无用的链接
for conn in del_list:
r_list.remove(conn)
del_list.clear()
客户端:
import socket
client = socket.socket()
client.connect(('127.0.0.1',8081))
while True:
client.send(b'hello world')
data = client.recv(1024)
print(data)
3、IO多路复用(IO MulTIplexing)
"IO 多路复用" 是一种用于监听多个网络连接的技术,以提高网络程序的性能和效率。常用的 IO 多路复用技术有 select、poll、epoll
。
在 Python 中,可以使用 select 模块来实现 IO 多路复用。select 模块提供了三个函数:select、poll、epoll
,可以在不同的平台上使用不同的函数来实现 IO 多路复用。它们都是用来监听多个文件描述符(socket)的读写情况,以实现对多个socket的高效管理。
select
:select 是最早的 I/O 多路复用 API 之一,并在大多数 Unix 类系统上广泛支持。select 可以监视大量的文件描述符,但它有许多限制,例如具有固定的最大文件描述符数量(1024
),以及当正在监视的文件描述符列表更改时受到竞争条件的影响。poll
:poll 是为了解决 select 中的一些限制而引入的,在大多数 Unix 类系统上广泛支持。poll 可以监视比 select 更多的文件描述符(无限制),并且它还提供了关于每个文件描述符状态的更多信息。epoll
:epoll 被引入为 poll 和 select 的更有效替代品,并可在 Linux 系统上使用。epoll 具有许多比 poll 和 select 更有效的性能优势,例如更快更可扩展的设计,以及能够以较低开销监视大量文件描述符的能力。
select、poll、epoll之间的区别:
select | poll | epoll | |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面。时间复杂度为O(1) |
最大连接数 | 1024(x86)或2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ct时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
它的流程如图:
- 管的对象只有一个的时候 其实IO多路复用连阻塞IO都比不上!!!但是IO多路复用可以一次性监管很多个对象
- 监管机制是操作系统本身就有的 如果你想要用该监管机制(select)需要,
- 你导入对应的select模块
示例如下:
import socket
import select
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
server.setblocking(False)
read_list = [server]
while True:
r_list, w_list, x_list = select.select(read_list, [], [])
"""
帮你监管
一旦有人来了 立刻给你返回对应的监管对象
"""
# print(res) # ([<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>], [], [])
# print(server)
# print(r_list)
for i in r_list: #
"""针对不同的对象做不同的处理"""
if i is server:
conn, addr = i.accept()
# 也应该添加到监管的队列中
read_list.append(conn)
else:
res = i.recv(1024)
if len(res) == 0:
i.close()
# 将无效的监管对象 移除
read_list.remove(i)
continue
print(res)
i.send(b'hello python')
# 客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
client.send(b'hello world')
data = client.recv(1024)
print(data)
4、异步IO(Asynchronous IO)
异步IO模型是所有模型中效率最高的 也是使用最广泛的 。先看一下它的流程:
"""
异步IO模型是所有模型中效率最高的 也是使用最广泛的
相关的模块和框架
模块: asyncio模块
异步框架:sanic tronado twisted
速度快!!!
"""
import threading
import asyncio
@asyncio.coroutine
def hello():
print('hello world %s'%threading.current_thread())
yield from asyncio.sleep(1) # 换成真正的IO操作
print('hello world %s' % threading.current_thread())
loop = asyncio.get_event_loop()
tasks = [hello(),hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
进程、线程与协程的关系与区别
进程、线程和协程是操作系统中用来管理程序执行的三种不同的技术。
- 进程:进程是操作系统中最基本的资源分配单元,是程序的实体。它是系统进行资源分配和调度的独立单位。每个进程都有独立的内存空间,因此在一个进程中出现故障不会影响其他进程。
- 线程:线程是进程的一个执行单元,是操作系统分配资源的最小单元。多线程可以共享进程的资源,因此线程间的通信和协作比进程间更加方便。不幸的是,线程之间存在竞争关系,并且当一个线程出现故障时,整个进程都会受到影响。
- 协程:协程是一种在单线程中执行多任务的机制,是线程的一种特殊形式。它不同于多线程,因为它不会独立分配资源,而是在一个线程中共享资源。因此,协程的实现比线程更加轻量,可以提高程序的效率。不过,协程的实现也比线程更加复杂,因为需要编写更多的代码来协调任务的。
详细解释:
链接:https://juejin.cn/post/7198505494251601981