How Tornado’s IOLoop Works: Deep Dive into Callbacks, Futures, and Coroutines
This article explains the inner workings of Tornado's IOLoop, covering its three core components—callback handling, timeout scheduling, and I/O event processing—while also detailing the Future class and the gen.coroutine implementation that together enable asynchronous programming in Python.
Overview of IOLoop
IOLoop is the core scheduling module of the Tornado framework. It drives all asynchronous events by repeatedly executing a while True loop that processes callbacks, timeouts, and I/O events.
1. Callbacks
Callbacks are stored in self._callbacks and executed on each loop iteration. They are added via IOLoop.instance().add_callback() and are the basic building blocks for breaking business logic into discrete tasks.
def add_future(self, future, callback):
# Schedule a callback when the given Future is finished.
assert is_future(future)
callback = stack_context.wrap(callback)
future.add_done_callback(lambda future: self.add_callback(callback, future))2. Timeouts (due_timeouts)
Timeouts are scheduled callbacks that run after a specified delay. The loop calculates the nearest timeout and uses that value as the poll timeout for the underlying epoll/kqueue implementation, ensuring timely execution without busy‑waiting.
self.add_timeout(self.time() + 5, lambda: future.set_result('set future is done'))3. I/O Event Handling
IOLoop registers file descriptors with add_handler(fd, handler, events). When the underlying poll returns ready descriptors, the corresponding handler functions are invoked, allowing network servers, HTTP clients, and other I/O‑bound components to operate asynchronously.
Future
The Future class represents a value that will become available later. It is used throughout Tornado to bridge callbacks and coroutines. The most important methods are add_done_callback and set_result.
def add_done_callback(self, fn):
if self._done:
fn(self)
else:
self._callbacks.append(fn)
def set_result(self, result):
self._result = result
self._set_done()
def _set_done(self):
self._done = True
for cb in self._callbacks:
try:
cb(self)
except Exception:
app_log.exception('Exception in callback %r for %r', cb, self)
self._callbacks = Nonegen.coroutine
The coroutine decorator transforms a generator function into a function that returns a Future. It runs the generator, automatically handling yielded Future objects (or lists of them) and resuming the generator when those futures complete.
def _make_coroutine_wrapper(func, replace_callback=False):
@functools.wraps(func)
def wrapper(*args, **kwargs):
future = TracebackFuture()
if replace_callback and 'callback' in kwargs:
callback = kwargs.pop('callback')
IOLoop.current().add_future(future,
lambda f: callback(f.result()))
try:
result = func(*args, **kwargs)
except (Return, StopIteration) as e:
result = getattr(e, 'value', None)
except Exception:
future.set_exc_info(sys.exc_info())
return future
if isinstance(result, types.GeneratorType):
try:
orig_stack_contexts = stack_context._state.contexts
yielded = next(result)
if stack_context._state.contexts is not orig_stack_contexts:
yielded = TracebackFuture()
yielded.set_exception(StackContextInconsistentError(
'stack_context inconsistency (probably caused by yield within a "with StackContext" block)'))
except (Return, StopIteration) as e:
future.set_result(getattr(e, 'value', None))
return future
except Exception:
future.set_exc_info(sys.exc_info())
return future
runner = Runner(result, future, yielded)
try:
return future
finally:
future = None
else:
future.set_result(result)
return future
return wrapperRunner
Runnerdrives the generator produced by a coroutine. It registers the yielded Future with the current IOLoop, and when that future completes it sends the result back into the generator, handling successive yields until the generator finishes.
class Runner(object):
def __init__(self, result, future, yielded):
self.result = result
self.future = future
self.yielded = yielded
self.handle_yield(yielded)
def handle_yield(self, yielded):
if isinstance(yielded, Future) and not yielded.done():
IOLoop.current().add_future(yielded, self.run)
else:
self.run(yielded)
def run(self, yielded=None):
try:
if yielded is not None:
yielded = self.result.send(yielded)
else:
yielded = next(self.result)
except StopIteration as e:
self.future.set_result(getattr(e, 'value', None))
return
except Exception:
self.future.set_exc_info(sys.exc_info())
return
self.handle_yield(yielded)Example Usage
The following example demonstrates a simple asynchronous sum using Future and coroutine. It schedules a timeout that resolves the future after three seconds, then yields the future inside the coroutine.
from tornado import ioloop
from tornado.gen import coroutine
from tornado.concurrent import Future
@coroutine
def async_sum(a, b):
future = Future()
ioloop.IOLoop.instance().add_timeout(ioloop.time() + 3,
lambda: future.set_result(a + b))
result = yield future
raise Return(result)
def main():
f = async_sum(2, 3)
ioloop.IOLoop.instance().start()
if __name__ == '__main__':
main()Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
