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)

多路复用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

异步IO就是用户进程发起read操作之后就立马去做其他事了,不会对用户进程产生阻塞,效率最高。当数据准备完成并拷贝到内存之后,kernel会给用户进程发送一个信号,告诉它read操作完成了。和非阻塞IO想比,它不用用户不断的去检查。异步IO在学爬虫的时候再用。

原文地址:https://www.cnblogs.com/lshedward/p/10250957.html