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

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的理解和深入分析。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值