本文的測試環境是在MacOS,因此使用的多路復用的網絡IO為kqueue而不是epoll,對應的IOLoop實例對象也是KQueueIOLoop。
在介紹Epoll模式的筆記中,最后寫了一個tornado的使用epoll的例子。這個例子是如何工作的呢?下面來讀一讀tornado的源碼。
啟動一個tornado server很簡單,只需要下面幾code:
io_loop = tornado.ioloop.IOLoop.current()
callback = functools.partial(connection_ready, sock)
io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
io_loop.start()
tornado.ioloop.IOLoop.current()
實際上是創建一個IO循環的對象,這里是KQueueIOLoop,Linux的系統則是EPollIOLoop。
下面是current的源碼,該方法目的就是從local線程中獲取KQueueIOLoop(如果存在的話,否則則新建一個)
@staticmethod
def current(instance=True):
current = getattr(IOLoop._current, "instance", None)
if current is None and instance:
return IOLoop.instance()
return current
程序首先判斷 IOLoop._current對象(_current對象是一個線程local)的instance屬性,如果沒有current,則調用IOLoop.instance()方法創建一個IOLoop的實例作為currnet返回。由于tornado的包裝,實際上IOLoop返回的并不是IOLoop的實例對象,而是KQueueIOLoop實例對象。
為什么IOLoop實例化的對象KQueueIOLoop呢?想知道答案就得揭開IOLoop.instance()神秘面紗,表面上看,該方法創建的IOLoop實例對象,并綁定到IOLoop._instance上。
@staticmethod
def instance():
if not hasattr(IOLoop, "_instance"):
with IOLoop._instance_lock:
if not hasattr(IOLoop, "_instance"):
# 新實例要經過兩次check檢查
IOLoop._instance = IOLoop()
return IOLoop._instance
IOLoop繼承自Configurable基類,IOLoop 自身沒有常見的初始化"構造函數"(init)。顯然需要再查看Configurable基類。不看不知道,一看tornado的作者還真會玩。Configurable是一個設計很精巧的類,通過不同子類的繼承來適配。基類在子類創建的時候做一些適配的事情。相比init, new稱之為構造函數更準確。
class Configurable(object):
def __new__(cls, *args, **kwargs):
base = cls.configurable_base()
init_kwargs = {}
if cls is base:
# 通過調用configured_class方法,可以綁定 base.__impl_class 為epoll還是kqueue。
impl = cls.configured_class()
if base.__impl_kwargs:
init_kwargs.update(base.__impl_kwargs)
else:
impl = cls
init_kwargs.update(kwargs)
# impl 對象為對應的網絡io模式,對于unix系統,因此這里是 kqueue方式,即位KQueueIOLoop類。
instance = super(Configurable, cls).__new__(impl)
# 通過initialize 方法,傳接 并返回,這個就是上面current對象,即
instance.initialize(*args, **init_kwargs)
return instance
IOLoop在創建的時候,通過基類new方法調用子類的configurable_base和configurable_default適配不同子類的特性。這里通過IOLoop的configurable_default方法選擇了unix系統的kqueue方式。
@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
根據平臺確定了impl為kqueue之后,將會通過new創建實例對象,就是這一步,創建了KQueueIOLoop而不是IOLoop的對象。Configurable自身不定義initialize。這里就調用了KQueueIOLoop的initialize方法。
class KQueueIOLoop(PollIOLoop):
def initialize(self, **kwargs):
super(KQueueIOLoop, self).initialize(impl=_KQueue(), **kwargs)
KQueueIOLoop的方法很簡單,其中實現了一個_KQueue,這個類用于操作unix系統上的kqueue的網絡io相關封裝,例如注冊事件,poll調用等。然后KQueueIOLoop帶用其父類(PollIOLoop)的initialize方法。有沒有發現,調用的控制權一直在各個父類基類中跳轉。大概是 IOLoop -> Configurable -> IOLoop -> KQueueIOLoop -> PollOLoop -> IOLoop -> PolIOLoop。
class PollIOLoop(IOLoop):
def initialize(self, impl, time_func=None, **kwargs):
# 調用父類的IOLoop的initialize方法
super(PollIOLoop, self).initialize(**kwargs)
# _KQueue類
self._impl = impl
PollIOLoop繼承自IOLoop,PollIOLoop調用其父類的initialize方法。此時調用make_current為None,因此又會調用IOLoop.current()的方法,怎么又是IOLoop.current?我們不就是從客戶端邏輯(相對于庫)調用這個方法進來的么?注意,不同于第一次客戶端調用的時候,當時intances是True。也就是此時直接返回IOLoop._current.instance,前面正是因為current為None,才需要通過IOLoop的創建對象。當然此時current為None,即直接返回None。接下來自然運行make_current方法。
def initialize(self, make_current=None):
if make_current is None:
if IOLoop.current(instance=False) is None:
self.make_current()
elif make_current:
if IOLoop.current(instance=False) is None:
raise RuntimeError("current IOLoop already exists")
self.make_current()
make_current方法干點啥好呢?當然你肯定想到了,既然我們之前IOLoop.current方法是為了獲取IOLoop._current.instance,并且一直為None,那么make_current正好填補這個空白,創建一個綁定就好嘛。
def make_current(self):
"""Makes this the `IOLoop` for the current thread.
將IOLoop實例對象綁定到local線程_current的instance屬性。
"""
IOLoop._current.instance = self
的確,make_current把當前的類實例(KQueueIOLoop)創建并綁定。通過前面巧妙的設計,根據平臺選擇了網絡io的模式。接下來還得根據io模式綁定IO監聽事件。繼續閱讀PollIOLoop,可以發現通過add_handler方法喝Waker實現。
class PollIOLoop(IOLoop):
def initialize(self, impl, time_func=None, **kwargs):
...
# posix風格的文件讀取操作,網絡io本質也是文件操作
self._waker = Waker()
# 添加事件綁定,前面條用子類KQueueIOLoop的時候,傳了_KQueue類
self.add_handler(self._waker.fileno(),
lambda fd, events: self._waker.consume(),
self.READ)
add_handler方法處理文件描述符,其中stack_context類通過wrap包裝一個上下文類似的東西。具體數據結構沒有仔細看,留待日后研究,總而言之,這個方法借助之前的_KQueue類注冊網絡io事件。
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)
此時,ioloop對象成功的創建。創建ioloop對象之后,server還不回啟動,需要調用start啟動。在啟動之前,也需要通過add_hanndler綁定事件函數。至于start的工作原理,下回再研究。