Python并发编程之IO模型
IO模型分为5种,分别是同步IO、异步IO、阻塞IO、非阻塞IO和信号驱动IO。信号驱动IO(signal driven IO)在实际中不常用,所以这里不做讨论。
一、阻塞IO(blocking IO)
在linux中,默认所有的socket都是blocking。我们一般使用的socket编程就是阻塞IO,就是在等待数据和往内存拷贝数据的时候会被阻塞住。
# server from socket import * from threading import Thread server = socket(AF_INET, SOCK_STREAM) server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('127.0.0.1', 8080)) server.listen(5) def communicate(conn): while True: try: data = conn.recv(1024) if not data: break conn.send(data.upper()) except ConnectionError: break conn.close() while True: print('starting......') conn, addr = server.accept() print(addr) t = Thread(target=communicate, args=(conn,)) t.start() # client from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8080)) while True: res = input('>>>').strip() if not res: continue client.send(res.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8')) client.close()
总结:这个并没有解决遇到IO切换,只是开启了多个线程去干通信的活,每个线程遇到IO还是阻塞着。虽然解决个IO的问题,但是随着开的多线程越来越多,消耗也越来越大。
二、非阻塞IO
linux下,可以通过设置socket使其变为non-blocking。当用户进程请求数据的时候,如果kernel没有准备好,它不会block用户进程,而是立刻返回一个error。用户判断是error,知道数据还没准备好,就可以去做别的事了。一旦kernel中的数据准备好了,并且再次收到了用户进程的system call,那么它马上就把数据拷贝到用户内存(这一阶段仍然是阻塞)。
说白了在非阻塞IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有
# server from socket import * server = socket(AF_INET, SOCK_STREAM) server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('127.0.0.1', 8080)) server.listen(5) server.setblocking(False) print('starting...') rlist = [] wlist = [] while True: try: conn, addr = server.accept() rlist.append(conn) print(rlist) except BlockingIOError: # print('干其他的活') # 收消息 del_rlist = [] for conn in rlist: try: data = conn.recv(1024) if not data: del_rlist.append(conn) continue wlist.append((conn, data.upper())) except BlockingIOError: continue except Exception: conn.close() del_rlist.append(conn) # 发消息 del_wlist = [] for item in wlist: try: conn = item[0] data = item[1] conn.send(data) del_wlist.append(item) except BlockingIOError: # 一旦出异常,就说明内存满了 pass # 内存满了就不会发送成功,那就把它留在列表里,下次在发一次 for item in wlist: wlist.remove(item) for conn in del_rlist: rlist.remove(conn) server.close() # client from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8080)) while True: res = input('>>>').strip() if not res: continue client.send(res.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8')) client.close()
多路复用IO可以让单个进程同时处理多个网络iO。基本原理是select/epoll这个功能会不断的轮询负责的所有socket,当某个socket有数据到达了,就通知用户进程。
多路复用IO相当于一个中介。中介会代理用户先问一次操作系统数据有没有准备好,如果没准备好就等着,等数据到达缓存区了,操作系统会告诉中介数据来了,中介会告诉套接字数据来了,然后套接字在像操作系统发一个recvfrom的系统调用,去接收数据。多路复用的性能还不如阻塞IO,因为它多了一个中介的交互环节,而阻塞IO是数据到达缓存区了,操作系统直接告诉套接字去取数据。
那为什么我们还用它呢?关键在于你作为中介,你可以代理多个用户。所以在检测的IO个数只有一个的情况下还不如用阻塞IO,检测多个套接字的时候再用多路复用IO。
# server from socket import * import select server = socket(AF_INET, SOCK_STREAM) server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('127.0.0.1', 8080)) server.listen(5) server.setblocking(False) print('starting...') rlist = [server, ] wlist = [] wdata = {} while True: rl, wl, xl = select.select(rlist, wlist, [], 0.5) # 第三个是处理异常的列表。每隔0.5秒去问一下操作系统 print('rl', rl) print('wl', wl) for sock in rl: if sock == server: conn, addr = sock.accept() rlist.append(conn) else: try: data = sock.recv(1024) if not data: sock.close() rlist.remove(sock) continue wlist.append(sock) wdata[sock] = data.upper() except Exception: sock.close() rlist.remove(sock) for sock in wl: data = wdata[sock] sock.send(data) wlist.remove(sock) wdata.pop(sock) server.close() # client from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8080)) while True: res = input('>>>').strip() if not res: continue client.send(res.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8')) client.close()
四、异步IO