• 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在学爬虫的时候再用。

  • 相关阅读:
    java_类承继其他类的内部类例子
    java_接口和抽象类的区别
    java_数组作缓存池的不可变类实例
    C++_归并排序(纯C版)
    C++_归并排序
    C++_快速排序(纯C版本)
    C++_快速排序
    C++_直接插入排序(纯C版)
    C++_直接插入排序
    自定义比较器的用法
  • 原文地址:https://www.cnblogs.com/lshedward/p/10250957.html
Copyright © 2020-2023  润新知