美文网首页Python基础
从Tornado出发,理解非阻塞和异步

从Tornado出发,理解非阻塞和异步

作者: Maru | 来源:发表于2017-08-07 16:54 被阅读342次

(一)前言

在过去的两周时间里,在iOS之余把身体出卖给Python,写了几天的Tornado突然比较好奇它的非阻塞的实现,所以写下了这篇文章,权当记录吧。

(二)最简单的服务器实现

我们都知道,在经典的C/S架构的网络模型中,我们都是通过Socket编程来完成服务端与客户端的网络数据的交互的。那么,如果我们直接使用Socket编程来完成一个客户端,其实也并不是很难,大概的代码如下:

#coding:utf-8
import socket
from time import ctime

PORT = 8888
BUFSIZE = 1024
ADDR = ('127.0.0.1', PORT)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(ADDR)
sock.listen(5)

while True:
    pip, addr = sock.accept()
    while True:
        data = pip.recv(BUFSIZE)
        if not data:
            break
        pip.send('[%s] %s' % (ctime(), data))
    pip.close()
sock.close()

这段代码也不难理解:首先我们创建一个socket对象,然后将其绑定到本地地址以及8888端口,值得一提的是这里的listen(5)指的是最大的连接数。在while循环中,我们通过server.accept()来获得了一个新的嵌套字对象和绑定的地址,并且该对象可以进行数据的收发操作。当不再能够接收到数据的时候,我们就把本次的连接关闭。这样的连接示意图大概是这样的:

Socket通讯.png

但是这里存在两个问题:

  1. 连接的过程中存在着阻塞。
  2. 当一个连接尚未处理完毕,无法处理下一个连接。

很显然,对于现代的业务要求来说,这样的两个问题显然是我们没有办法接受的,那么为了解决这两个问题,我们首先要弄清楚这两个问题之所在。

(三)Socket缓冲区和阻塞模式

要知道,在我们进行socket通信的过程中,无论是read()还是write()都不是直接从网络读取或者说写入网络的。大致的流程是,从网卡到内核,内核写入内核缓冲区,最后socket从内核缓冲区拷贝到用户进程读取数据。

内核缓冲区:每当一个socket被创建之后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
缓冲区有以下几种特性:

  • I/O缓冲区在每个TCP套接字中单独存在;
  • I/O缓冲区在创建套接字时自动生成;
  • 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  • 关闭套接字将丢失输入缓冲区中的数据。
I/O缓冲区.png

但是所说的阻塞是什么呢?当用户进程发起recvform()调用的时候,系统首先会检查是否有准备好的数据,如果发现系统还没有准备好数据,缓冲区没有可以读取的数据,那么当前的线程就会阻塞(Blocking),直到数据拷贝到用户进程当中或者有错误发生才会返回。示意图如下:

阻塞IO模型.png

简单的来说,所谓的IO阻塞是指,IO系统调用(recvform)的时候,用户进程主动的等待了系统调用返回的结果

(四)简单的解决方案

那么,Tornado是如何解决这个问题的呢?我们可以看到,在这里我们的阻塞主要发生在从发出系统调用到内核缓冲区准备好数据的这段时间内。那么发出调用之后,用户进程可不可以不进入“睡眠”状态呢?

轮询

首先想到的一种方案是,我们能不能在发起recvform之后不阻塞进程呢,该而采用轮询的方式,不断的调用recvform,如果数据还没有准备好,那么返回一个EWOULDBLOCK的错误,直到内核缓冲区准备好了该有的数据再返回给我们一个成功的调用。这样与之前所述的阻塞模型相比,用户进程不会被IO调用所阻塞,每次调用都会立即返回结果,所以这就是另外一种IO模型 -- IO同步非阻塞模型。

同步非阻塞模型.png

然而这样的解决方案缺点也是非常的明显,我们把CPU浪费在了轮询的工作上面,这样的解决方案也明显看起来很愚蠢。

(五)select、poll和epoll

select出现于1983年的4.2BSD,我们可以通过它的调用来监视多个文件描述符(file descriptor)的数组,当select方法返回之后,数组中就绪的文件描述符就会被内核修改标志位,使得进程可以获得这些文件修饰符来进行后续的操作。

这难道不正是我们想要的么,我们可以通过select来当做代理来管理我们所创建的socket,当内核缓冲区的数据准备好的时候,我们再发起recvform调用,这样我们就可以避过了IO调用的阻塞。

这样的解决方案对应的IO模型就是 -- IO多路复用模型(I/O Multiplexing Model)

IO多路复用模型.png

虽然select可以支持几乎所有的平台,但是select还是有缺点的:

  1. 单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
  2. select 所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

基于以上的缺点,在1986年的System V Release 3诞生了poll,然而poll只改进了最大文件描述符的数量限制,从原来的1024放开到了理论上的无限,但是对于第二个缺点依旧没有很好的方法。

那有没有其他的方案呢?有,epoll!

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

所以,Tornado在实现的过程中也是参考了epoll,相当于Tornado的非阻塞的实现就是基于epoll实现的。

接下来我们也可以来看一下三者的对比:

  1. 支持一个进程所能打开的最大连接数
image.png
  1. FD剧增后带来的IO效率问题
image.png
  1. 消息传递方式
image.png

至此,我们也非常明了的完成了多路复用的三种具体实现函数的对比,也明白了Tornado使用epoll的原因。

(六)数据准备阶段的非阻塞

然而这就是完美的解决方案了么?明显不是,因为虽然我们避过了recvform的数据准备阶段的阻塞,但是我们调用epoll函数的时候还是处于阻塞的状态。如果在此状态也可以非阻塞岂不是更好?

这个时候就需要信号驱动模型(Signal-Driven I/O Model)了。

信号驱动模型.png

上图我们可以清楚的看到,相比于我们调用了epoll函数之后的阻塞,在信号驱动模型中,当我们发起sigaction的系统调用之后,改调用会立刻返回,使得我们的用户进程可以继续处理其他事物。当数据从内核来到内核缓冲区之后,内核会发起一个SIGIO的“回调”信号,这个时候,用户进程再调用recvform将数据从内核缓冲区拷贝到用户进程,这样就完成了数据准备阶段的非阻塞。

然而,比较了这么多的模型,我们都没有一个模型可以真正的实现异步,因为在调用recvform的时候,系统总是处于阻塞的状态,有没有什么办法可以从等待数据到缓冲区到拷贝数据到用户进程一直都保持用户进程通畅的呢?

(七)异步IO模型

在前面谈论的所有模型都是同步的,即在用户进程当中,总有函数调用会刮起等待其执行的结果,这样对于计算机的资源使用明显不是最高效合理的。

为了实现异步调用,我们就不能再使用revcform调用了,我们改用aio_read,流程示意图如下:

异步IO模型.png

aio_readaio_write都是Linux中的异步函数,两个函数分别提供了异步读取数据和写入数据的功能,当写入完毕用户进程就能接收到一个“callback”,然后处理接下来的事务。值得注意得是,这里的读取动作包含了之前的两个阶段:数据准备阶段和数据拷贝阶段,因此,当用户进程发起aio_read之后将完全不会阻塞进程,大大了提高了用户进程的并发能力。

(八)五中模型的对比

对比.png

正如我们所知,当请求线程在I/O操作完成之前一直处于阻塞状态,那么这个操作是一个同步的操作,反之就是异步操作。那么,阻塞模型,非阻塞模型,IO多路复用模型,以及信号驱动模型都属于同步模型,因为他们都调用了会产生阻塞的recvform,只有异步IO模型属于真正的异步。

(九)异步

既然,Tornado通过epoll来完成了接收socket的非阻塞操作,那么对于处理请求时的异步操作,Tornado又是如何实现的呢?

回调

不像Javascript、Swift等语言原生所拥有的闭包机制,Python并没有这些机制,但是Python还是可以做到回调的,使用Tornado中的@asynchronous装饰器可以达到异步回调的目的。

class AsyncHandler(RequestHandler):
    @asynchronous
    def get(self):
        http_client = AsyncHTTPClient()
        http_client.fetch("http://example.com",
                            callback=self.on_fetch)

    def on_fetch(self, response):
        do_something_with_response(response)
        self.render("template.html")

但是,一旦使用了该装饰器就一定要手动的调用self.finish(),因为当使用该装饰器之后,所处理的请求自动变为了长连接,并且在调用self.finish()之前一直处于pending的状态。

当然这样的实现方式还是稍显不友好一些,因为如果回调的函数和发起回调的函数分开书写,各段的回调逻辑散落在代码的各个角落,无论对于书写人员还是对于维护人员都是非常不友好的。索性我们还有另外一种异步的方式。

协程

通过@gen.coroutine这个装饰器,我们可以将上述的代码改写成这样:

class GenAsyncHandler(RequestHandler):
    @gen.coroutine
    def get(self):
        http_client = AsyncHTTPClient()
        response = yield http_client.fetch("http://example.com")
        do_something_with_response(response)
        self.render("template.html")

从实现原理上面来说,@gen.coroutine@asynchronous并无太大的区别,同样都是讲请求放为长连接并且状态置为pending。但是这里通过使用Python生成器的模式来将原来分开的调用和回调聚合在了一起,使得拥有了现代编程语言的“闭包”机制,因此,大多数情况之下我们更加推荐使用这种方式来编写异步的代码。

ThreadPoolExecutor

对于Tornado应用来说,还有第三种异步的方式就是ThreadPoolExecutor,具体的代码如下:

class GenAsyncHandler(RequestHandler):
    executor = ThreadPoolExecutor(10)
    @run_on_executor
    def get(self):
        http_client = AsyncHTTPClient()
        http_client.fetch("http://example.com")
        do_something_with_response(response)
        self.render("template.html")

ThreadPoolExecutor的异步实现方案与上述两个方案略有不同,当我们使用executor = ThreadPoolExecutor(10),系统就会默认帮我们创建一个线程池,在本次例子中我们创建了10个线程,当我们执行被@run_on_executor的时候,我们就会从线程池中拿取一个线程,然后在该线程之上执行代码,从而达到异步的效果。但是,缺点是当短时间处理大量的异步请求的时候,所有线程池中的线程都处于使用的状态,那么这样还是会导致阻塞。所以在使用改异步方法的时候一定要慎重选择。

感谢参考

IO模型
聊聊Linux 五种IO模型
Tornado
聊聊IO多路复用之select、poll、epoll详解
StackOverFlow

相关文章

  • 从Tornado出发,理解非阻塞和异步

    (一)前言 在过去的两周时间里,在iOS之余把身体出卖给Python,写了几天的Tornado突然比较好奇它的非阻...

  • 真正的 Tornado 异步非阻塞

    其中Tornado的定义是 Web 框架和异步网络库,其中他具备有异步非阻塞能力,能解决他两个框架请求阻塞的问题,...

  • Tornado入门(一)【简介】

    这个系列都是译自官方文档,地址: tornado Tornado是基于Python实现的异步网络框架,它采用非阻塞...

  • tornado框架

    1.支持异步非阻塞,底层使用epoll,IO多路复用2.tornado不是基于wsgi,而是基于tornado,运...

  • Tornado异步非阻塞详解

    前言:鉴于Google了大片关于Tornado框架关于其异步非阻塞的实现方法和缘由结果都不尽理想,在此写一篇个人了...

  • tornado异步非阻塞请求

    基础知识 首先对同步/异步、阻塞/非阻塞、以及tornado中asyncio进行了解,参考下面的文章1.pytho...

  • 同步、异步、阻塞、非阻塞,这下明白了

    同步阻塞,同步非阻塞,异步阻塞,异步非阻塞... 晕! 头! 转! 向! 对于小白来说,理解这些概念太难了。搜索这...

  • 区分同步异步阻塞非阻塞区别

    “阻塞”与"非阻塞"与"同步"与“异步"不能简单的从字面理解,提供一个从分布式系统角度的回答。 同步与异步同步和异...

  • Java AIO基础

    Java AIO(异步IO)特性是在Java7引入的。 [TOC] 同步异步、阻塞非阻塞的理解 同步和异步 同步和...

  • 同步异步,阻塞非阻塞

    “阻塞”与"非阻塞"与"同步"与“异步"不能简单的从字面理解,提供一个从分布式系统角度的回答。1.同步与异步同步和...

网友评论

    本文标题:从Tornado出发,理解非阻塞和异步

    本文链接:https://www.haomeiwen.com/subject/uqtelxtx.html