python IO多路复用实践

最近几天一直在看tornado源码,发现torando虽然标榜使用异步模型实现, 但是实际上是使用IO多路复用实现的事件循环,为了能对 IO多路复用加深印象,决定自己实现一个简易的HTTP客户端对比一下同步客户端和IO多路复用客户端的性能差别。

废话少说, 现在先来看看同步客户端与IO多路复用客户端最直观的区别,首先使用tornado实现一个简单的服务端:

# -*- coding:utf-8 -*-
from tornado.web import RequestHandler, Application
from tornado.ioloop import IOLoop
import tornado.gen

class TestHandle(RequestHandler):

    async def get(self, *args, **kwargs):
        await tornado.gen.sleep(1)
        self.write("OK")


if __name__ == '__main__':
    app = Application([('/', TestHandle)])
    app.listen(8080)
    IOLoop.current().start()

我们让服务端在处理请求时暂停1秒,以便能够更方便观察出两种方式实现的客户端区别,接下来先实现同步客户端:

# -*- coding:utf-8 -*-
import socket
import time

REQUEST_STR = '{method} {path} HTTP/1.0\r\n\r\n'


def block_client(hostname, port, method, path):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((hostname, port))
    sock.send(REQUEST_STR.format(method=method, path=path).encode())
    response_body = []
    while True:
        response_stream = sock.recv(1024)
        if not response_stream:
            return b"".join(response_body).decode()
        response_body.append(response_stream)


if __name__ == '__main__':
    count = 3
    start_time = time.time()
    for _ in range(3):
        block_client("127.0.0.1", port=8080, method="GET", path="/")
    print("{} 请求{}次, 运行时间: {:.1f}秒".format(block_client.__name__, count, time.time() - start_time))

启动服务端, 开始测试同步客户端请求服务端需要多少时间:

C:\Users\Administrator>python client.py
block_client 请求3次, 运行时间: 3.0秒

不出所料,请求三次由于服务器暂停了1秒总计使用时间为3秒。接下来实现IO多路复用客户端,来看看IO多路复用的表现:

from selectors import DefaultSelector, EVENT_WRITE, EVENT_READ
from functools import partial
import socket
import time

REQUEST_STR = '{method} {path} HTTP/1.0\r\n\r\n'
JOBS_COUNT = 0
select = DefaultSelector()


class NoBlockClient:
    def __init__(self):
        self.result = []

    def request(self, method, hostname, port, path):
        global JOBS_COUNT
        JOBS_COUNT += 1
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setblocking(False)
        try:
            sock.connect((hostname, port))
        except BlockingIOError:
            pass
        select.register(sock.fileno(), EVENT_WRITE, partial(self._send, sock, method, path))

    def _send(self, sock, method, path):
        select.unregister(sock.fileno())
        sock.send(REQUEST_STR.format(method=method, path=path).encode())
        select.register(sock.fileno(), EVENT_READ, partial(self._recv, sock))

    def _recv(self, sock):
        global JOBS_COUNT
        response_stream = sock.recv(1024)
        if not response_stream:
            select.unregister(sock.fileno())
            sock.close()
            JOBS_COUNT -= 1
        else:
            self.result.append(response_stream)

    def run(self):
        while JOBS_COUNT:
            events = select.select()
            for key, mask in events:
                callback = key.data
                callback()
        return self.result


start_time = time.time()
count = 3
no_block_client = NoBlockClient()

for _ in range(count):
    no_block_client.request("GET", "127.0.0.1", 8080, "/")
result = no_block_client.run()
print("{} 请求{}次, 运行时间: {:.1f}秒".format(NoBlockClient.__name__, count, time.time() - start_time))

运行客户端:

C:\Users\Administrator>python no_block_client.py
NoBlockClient 请求3次, 运行时间: 1.0秒

非常明显的看见时间上的区别,虽然代码看起来特别的复杂, 但是说的直白一点就是将每一个函数对应一个事件注册进sleelct中, 由sleect进行监控, 如果有事件触发就运行对应的函数。

我们分析下IO多路复用客户端代码:

1.select = DefaultSelector() 该行代码返回了当前平台IO多路复用最佳实现方式,分别为:select、poll、epoll、dev/poll、kqueue, 由于我是使用win来测试所以DefaultSelector()返回了select模型。

2.NoBlockClient.request函数目的是创建一个非阻塞套接字连接至目标服务器, 并将当前套接字注册进select中, 当其状态为可写时, 运行NoBlockClient._send相当于一个回调函数。

3.NoBlockClient._send负责将消息发送至已连接的服务器,最后如同NoBlockClient.request一样注册事件选择回调函数。

4.NoBlockClient._recv当注册事件为可读时, 将运行该函数, 读取服务器返回数据,至此一次完整的请求就结束了。

5.NoBlockClient.run函数主要为了让select开始循环监听这些注册事件的状态,并运行回调函数。

selectors模块是对select模块封装,使得我们不用在意当前平台需要使用什么模型,而是直接返回当前平台最佳的模型,并将各个模型的api进行整合,让使用者能够更方便的写出跨平台代码。

最经常使用的几种模型包括: select,、poll、 epoll,接下来所以说这几种模型的区别,优缺点以及大概的实现方式。

select: select将被注册的事件放入一个列表中并拷贝到内核空间进行监听,如果这些事件其中一个有了变化那么select将再次把包含事件的列表拷贝进用户空间,这就造成了资源上的极大浪费, 如果select只监听一个或者两个事件还好, 但是当select需要监听的事件越来越多时,select的性能将会直线下降。而且select将时间拷贝到用户空间时并不会告诉用户哪一个事件被触发了,而是要用户自己去遍历。因为select监听的事件越多性能越差所以通常系统内核都会对select模型监听数量进行限制,在python源码中(github: selectmodule.c)使用宏定义将win中select监听事件限制在了512:

当然select的最大优点则是几乎所有的平台都支持select。

2.poll: 其实现方式几乎与select相同, 所以select有的缺点poll也拥有,这里就不多说了。 详情见poll事件机制

3.epoll: epoll相比较select、poll有了质的改变,epoll将注册的事件都插入到了红黑树中,红黑树中的每一个节点都是一个注册的事件, 由于红黑树查询、插入、删除时间复杂度都是O(logn),所以epoll能够更加方便的对事件进行管理, 并且其在事件被触发时仅仅返回被触发的事件而不是像select全部返回, 这大大增加了效率。更加详细的epoll模型介绍见EPOLL的理解和深入分析

本篇文章所有内容仅仅为了我自己记忆, 其中可能有误还请指出,谢谢!

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值