mmxingye

导航

04 | pythonIO并发(IO多路复用、协程技术)

IO并发

IO 分类 (模型思想)

IO分类:阻塞IO ,非阻塞IO,IO多路复用,异步IO等

阻塞IO

1.定义:在执行IO操作时如果执行条件不满足则阻塞。阻塞IO是IO的默认形态。

2.效率:阻塞IO是效率很低的一种IO。但是由于逻辑简单所以是默认IO行为。

3.阻塞情况:

  • 因为某种执行条件没有满足造成的函数阻塞 (我们关注这个问题)
    e.g. accept input recv

  • 处理IO的时间较长产生的阻塞状态 (这个不太好解决)
    e.g. 网络传输,大文件读写

非阻塞IO

  1. 定义 :通过修改IO属性行为,使原本阻塞的IO变为非阻塞的状态。
  • 设置套接字为非阻塞IO (转身就走)

sockfd.setblocking(bool)
功能:设置套接字为非阻塞IO
参数:默认为True,表示套接字IO阻塞;设置为False则套接字IO变为非阻塞

"""
block_io.py
socket 非阻塞IO示例
"""

from socket import *
from time import *

# 日志文件
f = open('log.txt','a+')

# tcp 服务端
sockfd = socket()
sockfd.bind(('0.0.0.0',8888))
sockfd.listen(5)

# 非阻塞设置
# sockfd.setblocking(False)  #该对象的所有方法都变成了非阻塞的,于是有些函数会报异常 比如accept

# 超时时间
sockfd.settimeout(2)


while True:
    print("Waiting from connect...")
    try:
        connfd,addr = sockfd.accept()
    except (BlockingIOError,timeout) as e:
        sleep(2)
        f.write("%s : %s\n"%(ctime(),e))  # 写日志
        f.flush()
    else:
        print("Connect from",addr)
        data = connfd.recv(1024).decode()
        print(data)

  • 超时检测 :设置一个最长阻塞时间,超过该时间后则不再阻塞等待。 (等一等再走)

    sockfd.settimeout(sec)
    功能:设置套接字的超时时间
    参数:设置的时间

IO多路复用 🚩

  1. 定义

同时监控多个IO事件,当哪个IO事件准备就绪 内核感知到了发生就执行哪个IO事件。以此形成可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。

  1. 具体方案

select方法 : windows linux unix
poll方法: linux unix
epoll方法: linux

select 方法

rs, ws, xs=select(rlist, wlist, xlist[, timeout])
功能: 监控IO事件,阻塞等待IO发生
参数:rlist  列表  存放关注的等待发生的IO事件
      wlist  列表  存放关注的要主动处理的IO事件
      xlist  列表  存放关注的出现异常要处理的IO
      timeout  超时时间

返回值: rs 列表  rlist中准备就绪的IO
        ws 列表  wlist中准备就绪的IO
	xs 列表  xlist中准备就绪的IO
"""
select示例 在linux系统下示范
"""

from select import select
from socket import *

s = socket()
s.bind(('0.0.0.0', 8888))
s.listen(3)

f = open('log.txt', 'r+')

print("开始监控IO")
rs, ws, xs = select([s], [f], [])
print(rs)
print(ws)
print(xs)

🔧select 实现tcp服务

【1】 将关注的IO放入对应的监控类别列表
【2】通过select函数进行监控
【3】遍历select返回值列表,确定就绪IO事件
【4】处理发生的IO事件
"""
select tcp 服务
重点代码

思路分析:
1. 将关注的IO放入监控列表
2. 当IO就绪时通知select返回
3. 遍历返回值列表,处理就绪的IO
"""

from socket import *
from select import select

# 创建监听套接字
s = socket()
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(('0.0.0.0',8888))
s.listen(5)

# 设置关注的IO列表
rlist = [s]  # s 用于等待处理连接
wlist = []
xlist = []

# 循环IO监控
while True:
    # print("++++",rlist)
    rs,ws,xs = select(rlist,wlist,xlist)
    # print('----',rs,ws)
    # 遍历返回值列表,判断哪个IO就绪
    for r in rs:
        if r is s:
            c,addr = r.accept()
            print("Connect from",addr)
            rlist.append(c) # 增加新的关注的IO
        else:
            # 表明有客户端发送消息
            data = r.recv(1024).decode()
            if not data:
                # 客户端对出则取消对其关注
                rlist.remove(r)
                r.close()
                continue
            print(data)
            # r.send(b'OK')
            wlist.append(r)

    for w in ws:
        w.send(b'OK')
        wlist.remove(w)

    for x in xs: # pass
        pass

注意

wlist中如果存在IO事件,则select立即返回给ws
处理IO过程中不要出现死循环占有服务端的情况
IO多路复用消耗资源较少,效率较高

poll方法

p = select.poll()
功能 : 创建poll对象
返回值: poll对象
p.register(fd,event)   
功能: 注册关注的IO事件
参数:fd  要关注的IO
      event  要关注的IO事件类型
  	     常用类型:POLLIN  读IO事件(rlist)
		      POLLOUT 写IO事件 (wlist)
		      POLLERR 异常IO  (xlist)
		      POLLHUP 断开连接 
		  e.g. p.register(sockfd,POLLIN|POLLERR)

p.unregister(fd)
功能:取消对IO的关注
参数:IO对象或者IO对象的fileno
events = p.poll()
功能: 阻塞等待监控的IO事件发生
返回值: 返回发生的IO
        events格式  [(fileno,event),()....]
        每个元组为一个就绪IO,元组第一项是该IO的fileno,第二项为该IO就绪的事件类型

🔧poll_server 实现IO多路复用
【1】 创建套接字
【2】 将套接字register
【3】 创建查找字典,并维护
【4】 循环监控IO发生
【5】 处理发生的IO

"""
poll_server 完成tcp并发服务
重点代码

【1】 创建套接字
【2】 将套接字register
【3】 创建查找字典,并维护
【4】 循环监控IO发生
【5】 处理发生的IO
"""
from socket import *
from select import *

# 创建监听套接字,作为关注的IO
s = socket()
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(('0.0.0.0',8888))
s.listen(3)

# 创建poll对象
p = poll()

# 建立查找字典,通过IO的fileno查找io对象
# 始终与register的IO保持一直
fdmap = {s.fileno():s}

# 关注 s
p.register(s,POLLIN|POLLERR)

# 循环监控IO发生
while True:
    events = p.poll() # 阻塞等待IO发生
    # 循环遍历查看哪个IO准备就绪
    for fd,event in events:
        print(events)
        if fd == s.fileno():
            c,addr = fdmap[fd].accept()
            print("Connect from",addr)
            # 关注客户端连接套接字
            p.register(c,POLLIN|POLLHUP)
            fdmap[c.fileno()] = c  # 维护字典
        elif event & POLLIN:
            data = fdmap[fd].recv(1024).decode()
            if not data:
                p.unregister(fd) # 取消监控
                fdmap[fd].close()
                del fdmap[fd] # 从字典删除
                continue
            print(data)
            p.register(fdmap[fd],POLLOUT)
        elif event & POLLOUT:
            fdmap[fd].send(b'OK')
            p.register(fdmap[fd], POLLIN)

epoll方法

  1. 使用方法 : 基本与poll相同

    • 生成对象改为 epoll()
    • 将所有事件类型改为EPOLL类型
  2. epoll特点

    • epoll 效率比select poll要高 (因为其他两者 是在应用层准备好事件然后拷贝到内核,内核触发后再整体拷贝回应用层,然后再由应用层进行遍历完成具体是哪一个io事件。而epoll是直接register到内核开辟一个空间,当有某个对象的事件被触发就仅仅返回被触发的事件,省去了拷贝和遍历)
    • epoll 监控IO数量比select要多
    • epoll 的触发方式比poll要多 (EPOLLET边缘触发) (水平触发和边缘触发的概念)

🔧基于epoll的IO多路复用

"""
epoll_server 完成tcp并发服务
"""
from socket import *
from select import *

# 创建监听套接字,作为关注的IO
s = socket()
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(('0.0.0.0',8888))
s.listen(3)

# 创建epoll对象
ep = epoll()

# 建立查找字典,通过IO的fileno查找io对象
# 始终与register的IO保持一直
fdmap = {s.fileno():s}

# 关注 s
ep.register(s,EPOLLIN|EPOLLERR)

# 循环监控IO发生
while True:
    events = ep.poll() # 阻塞等待IO发生
    print("你有新的IO需要处理哦")
    # 循环遍历查看哪个IO准备就绪
    for fd,event in events:
        print(events)
        if fd == s.fileno():
            c,addr = fdmap[fd].accept()
            print("Connect from",addr)
            # 关注客户端连接套接字
            ep.register(c,EPOLLIN|EPOLLET) # 设置边缘触发
            fdmap[c.fileno()] = c  # 维护字典
        elif event & EPOLLIN:
            data = fdmap[fd].recv(1024).decode()
            if not data:
                ep.unregister(fd) # 取消监控
                fdmap[fd].close()
                del fdmap[fd] # 从字典删除
                continue
            print(data)
            ep.unregister(fd) # 先取消关注再重新添加
            ep.register(fdmap[fd], EPOLLOUT)
        elif event & POLLOUT:
            fdmap[fd].send(b'OK')
            ep.unregister(fd)
            ep.register(fdmap[fd], EPOLLIN)

协程技术

基础概念

  1. 定义:纤程,微线程。是允许在不同入口点不同位置暂停或开始的计算机程序,简单来说,协程就是可以暂停执行的函数。

  2. 协程原理 : 记录一个函数的上下文,协程调度切换时会将记录的上下文保存,在切换回来时进行调取,恢复原有的执行内容,以便从上一次执行位置继续执行。

  3. 协程优缺点

优点

  1. 协程完成多任务占用计算资源很少
  2. 由于协程的多任务切换在应用层完成,因此切换开销少
  3. 协程为单线程程序(常用于IO密集型程序),无需进行共享资源同步互斥处理

缺点

协程的本质是一个单线程,无法利用计算机多核资源


扩展延伸@标准库协程的实现

python3.5以后,使用标准库asyncio和async/await 语法来编写并发代码。asyncio库通过对异步IO行为的支持完成python的协程。虽然官方说asyncio是未来的开发方向,但是由于其生态不够丰富,大量的客户端不支持awaitable需要自己去封装,所以在使用上存在缺陷。更多时候只能使用已有的异步库(asyncio等),功能有限


第三方协程模块

  1. greenlet模块
  • 安装 : sudo pip3 install greenlet

  • 函数

greenlet.greenlet(func)
功能:创建协程对象
参数:协程函数

g.switch()
功能:选择要执行的协程函数
"""
协程行为示例
"""

from greenlet import greenlet


def fun1():
    print("执行 fun1")
    gr2.switch()
    print("结束 fun1")
    gr2.switch()


def fun2():
    print("执行 fun2")
    gr1.switch()
    print("结束 fun2")


# 将函数变为协程
gr1 = greenlet(fun1)
gr2 = greenlet(fun2)

gr1.switch()  # 选择执行的协程函数

  1. gevent模块
  • 安装:sudo pip3 install gevent

  • 函数

gevent.spawn(func,argv)
功能: 生成协程对象
参数:func  协程函数
     argv  给协程函数传参(不定参)
返回值: 协程对象

gevent.joinall(list,[timeout])
功能: 阻塞等待协程执行完毕
参数:list  协程对象列表
     timeout 超时时间

gevent.sleep(sec)
功能: gevent睡眠阻塞
参数:睡眠时间

* gevent协程只有在遇到gevent指定的阻塞行为时才会自动在协程之间进行跳转
如gevent.joinall(),gevent.sleep()带来的阻塞
"""
gevent生成协程演示
"""

import gevent

# 协程函数
def foo(a,b):
    print("Running foo ...",a,b)
    gevent.sleep(3)
    print("Foo again..")

def bar():
    print("Running bar ...")
    gevent.sleep(2)
    print("Bar again..")

# 生成协程对象
f = gevent.spawn(foo,1,2)
g = gevent.spawn(bar)

gevent.joinall([f,g]) #阻塞等待f,g代表的协程执行完毕
  • monkey脚本 (是用来配合 gevent 的阻塞行为的!!让它看上去更像普通程序)

作用:在gevent协程中,协程只有遇到gevent指定类型的阻塞才能跳转到其他协程,因此,我们希望将普通的IO阻塞行为转换为可以触发gevent协程跳转的阻塞,以提高执行效率。

转换方法:gevent 提供了一个脚本程序monkey,可以修改底层解释IO阻塞的行为,将很多普通阻塞转换为gevent阻塞。

使用方法

【1】 导入monkey

		from gevent  import monkey

【2】 运行相应的脚本,例如转换socket中所有阻塞

		monkey.patch_socket()

【3】 如果将所有可转换的IO阻塞全部转换则运行all

		monkey.patch_all()

【4】 注意:脚本运行函数需要在对应模块导入前执行

"""
gevent生成协程演示
"""

import gevent
from gevent import monkey
monkey.patch_time() # 修改对time模块中阻塞的解释行为
from time import sleep

# 协程函数
def foo(a,b):
    print("Running foo ...",a,b)
    # gevent.sleep(3)
    sleep(3)
    print("Foo again..")

def bar():
    print("Running bar ...")
    # gevent.sleep(2)
    sleep(2)
    print("Bar again..")

# 生成协程对象
f = gevent.spawn(foo,1,2)
g = gevent.spawn(bar)

gevent.joinall([f,g]) #阻塞等待f,g代表的协程执行完毕

🔧基于协程的 服务器端模型

"""
gevent server 基于协程的tcp并发
思路 : 1. 每个客户函数端设置为协程
      2. 将socket模块下的阻塞变为可以触发协程跳转
"""
import gevent
from gevent import monkey
monkey.patch_all() # 执行脚本,修改socket
from socket import *

def handle(c):
    while True:
        data = c.recv(1024).decode()
        if not data:
            break
        print(data)
        c.send(b'OK')
    c.close()

# 创建tcp套接字
s = socket()
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(('0.0.0.0',8888))
s.listen(5)

# 循环接收来自客户端连接
while True:
    c,addr = s.accept()
    print("Connect from",addr)
    # handle(c) # 处理具体客户端请求
    gevent.spawn(handle,c) # 协程方案

posted on 2022-05-24 20:44  独立树  阅读(166)  评论(0编辑  收藏  举报