tornado源碼解析之IOLoop

0. 簡介

tornado是一個用Python語言寫成的Web服務器兼Web應用框架,由FriendFeed公司在本身的網站FriendFeed中使用,被Facebook收購之後框架以開源軟件形式開放給大衆。python

tornado最大的特色就是其支持異步IO,因此它有着優異的性能。下表是和一些其餘Web框架與服務器的對比:(處理器爲 AMD Opteron, 主頻2.4GHz, 4核) (來源wikipedia)linux

服務 部署 請求/每秒
Tornado nginx, 4進程 8213
Tornado 1個單線程進程 3353
Django Apache/mod_wsgi 2223
web.py Apache/mod_wsgi 2066
CherryPy 獨立 785

先來看看hello world的例子。^_^nginx

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

from tornado.options import define, options

define("port", default=8888, help="run on the given port", type=int)


class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")


def main():
    tornado.options.parse_command_line()
    application = tornado.web.Application([
        (r"/", MainHandler),
    ])
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()


if __name__ == "__main__":
    main()

運行:git

$ python3 helloworld.py

咱們就獲得一個web server監聽在8888端口。用curl命令get一下,就返回了"Hello, world"。github

tornado的代碼結構能夠在其官網瞭解,本文着重分析IOLoop的實現。web

1. IOLoop

1.1 http交互的大體過程

介紹IOLoop以前咱們先看看http server和http client交互的一個大體過程。apache

http

server端監聽在某個端口,client端發送請求過來,server處理後返回,而後繼續等待下一個請求,周而復始。若是用socket那一坨來描述的話就是:segmentfault

1. server.py
================================================================
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(address)
s.listen(backlog)
While True:
    connection = s.accept()
    do_something()
    connection.send()
    connection.close()
    
2. client.py
=================================================================
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect()
s.send()
s.recv()
s.close()

1.2 聊聊阻塞與非阻塞

所謂阻塞,就是進程正在等待某些資源(如IO),而處於等待運行的狀態(不佔用CPU資源)。好比connect(("google.com", 80))返回以前進程都是阻塞的,在它下面的語句得不到執行,除非connect返回。bash

很顯然阻塞式的IO模型有個缺點就是併發量不大,試想若是server進程在do_something()處阻塞,而這時另外有個客戶端試圖連進來,則可能得不到響應。服務器

提升併發量有幾種實現方式:多線程(一個鏈接fork一個線程去處理);多進程(一個鏈接fork一個子進程去處理)(apache);事件驅動(nginx, epoll)等。tornado就是基於epoll(Linux)事件驅動模型實現的。

固然它們有各自的優缺點,此文不詳述,有興趣的讀者能夠自行google之。^_^

關於IO模型,epoll, 同步,異步,阻塞,非阻塞的概念,能夠參考這兩篇文章:
https://segmentfault.com/a/11...

http://blog.csdn.net/historya...

1.3 IOLoop實現

1.3.1 IOLoop配置

前文說到tornado是基於epoll事件驅動模型,也不徹底正確,tornado其實是根據平臺選擇底層驅動。請看IOLoop類的configurable_default方法:

@classmethod
    def configurable_default(cls):
        if hasattr(select, "epoll"):
            from tornado.platform.epoll import EPollIOLoop
            return EPollIOLoop
        if hasattr(select, "kqueue"):
            # Python 2.6+ on BSD or Mac
            from tornado.platform.kqueue import KQueueIOLoop
            return KQueueIOLoop
        from tornado.platform.select import SelectIOLoop
        return SelectIOLoop

這裏的IOLoop其實是個通用接口,根據不一樣平臺選擇:linux->epoll,BSD->kqueue,若是epoll和kqueue都不支持則選擇select(性能要差些)。

class IOLoop(Configurable):IOLoop繼承了Configurable類,Configurable類的__new__方法調用了configured_class方法:

def __new__(cls, *args, **kwargs):
        base = cls.configurable_base()
        init_kwargs = {}
        if cls is base:
            impl = cls.configured_class()
            if base.__impl_kwargs:
                init_kwargs.update(base.__impl_kwargs)
        else:
            impl = cls
        init_kwargs.update(kwargs)
        instance = super(Configurable, cls).__new__(impl)
        # initialize vs __init__ chosen for compatibility with AsyncHTTPClient
        # singleton magic.  If we get rid of that we can switch to __init__
        # here too.
        instance.initialize(*args, **init_kwargs)
        return instance

configured_class方法又調用了configurable_default方法:

@classmethod
    def configured_class(cls):
        # type: () -> type
        """Returns the currently configured class."""
        base = cls.configurable_base()
        if cls.__impl_class is None:
            base.__impl_class = cls.configurable_default()
        return base.__impl_class

因此當初始化一個IOLoop實例的時候就給IOLoop作了配置,根據不一樣平臺選擇合適的驅動。

1.3.2 IOLoop實例化

下面咱們來看IOLoop的實例化函數:

# Global lock for creating global IOLoop instance
    _instance_lock = threading.Lock()
    @staticmethod
    def instance():
        if not hasattr(IOLoop, "_instance"):
            with IOLoop._instance_lock:
                if not hasattr(IOLoop, "_instance"):
                    # New instance after double check
                    IOLoop._instance = IOLoop()
        return IOLoop._instance

很顯然,這裏是實現了一個全局的單例模式。確保多個線程也只有一個IOLoop實例。(思考一下:爲什要double check?if not hasattr(IOLoop, "_instance") ^_^)

1.3.3 實現epoll的接口(假設是在Linux平臺)

咱們知道epoll支持3種操做:

EPOLL_CTL_ADD    添加一個新的epoll事件
EPOLL_CTL_DEL    刪除一個epoll事件
EPOLL_CTL_MOD    改變一個事件的監聽方式

分別對應tornado.IOLoop裏面的三個函數:add_handler, remove_handler, update_handler

下面看看這三個函數:

def add_handler(self, fd, handler, events):
        fd, obj = self.split_fd(fd)
        self._handlers[fd] = (obj, stack_context.wrap(handler))
        self._impl.register(fd, events | self.ERROR)

    def update_handler(self, fd, events):
        fd, obj = self.split_fd(fd)
        self._impl.modify(fd, events | self.ERROR)

    def remove_handler(self, fd):
        fd, obj = self.split_fd(fd)
        self._handlers.pop(fd, None)
        self._events.pop(fd, None)
        try:
            self._impl.unregister(fd)
        except Exception:
            gen_log.debug("Error deleting fd from IOLoop", exc_info=True)

這裏的self._impl就是select.epoll(),使用方法能夠參考epoll接口。

1.3.4 事件驅動模型的大體思路

IOLoop的start()方法用於啓動事件循環(Event Loop)。

(部分源碼)
while self._events:
    fd, events = self._events.popitem()
    try:
        fd_obj, handler_func = self._handlers[fd]
        handler_func(fd_obj, events)
    except (OSError, IOError) as e:
        if errno_from_exception(e) == errno.EPIPE:
            # Happens when the client closes the connection
            pass
        else:
            self.handle_callback_exception(self._handlers.get(fd))
    except Exception:
        self.handle_callback_exception(self._handlers.get(fd))

大體的思路是:有鏈接進來(client端請求),就丟給epoll,順便註冊一個事件和一個回調函數,咱們主線程仍是繼續監聽請求;而後在事件循環中,若是發生了某種事件(如socket可讀,或可寫),則調用以前註冊的回調函數去處理。這和Node.js的思路是一致的。

1.3.5 關於cpu bound任務

tornado很適合處理IO bound的任務,若是遇到cpu bound的任務,則仍是會阻塞整個進程。這個時候就必須將耗時的任務丟到另外一個worker,或者隊列中去處理(如celery)。

1.3.6 其餘

IOLoop類還有其餘一些方法,多爲輔助函數,讀者能夠自行參考,此處不詳述。

行文比較草率,若有錯誤和不足之處,敬請指正。

下次繼續分析tornado其餘模塊。^_^

相關文章
相關標籤/搜索