284 lines
9.8 KiB
ReStructuredText
284 lines
9.8 KiB
ReStructuredText
Coroutines
|
|
==========
|
|
|
|
.. testsetup::
|
|
|
|
from tornado import gen
|
|
|
|
**Coroutines** are the recommended way to write asynchronous code in
|
|
Tornado. Coroutines use the Python ``yield`` keyword to suspend and
|
|
resume execution instead of a chain of callbacks (cooperative
|
|
lightweight threads as seen in frameworks like `gevent
|
|
<http://www.gevent.org>`_ are sometimes called coroutines as well, but
|
|
in Tornado all coroutines use explicit context switches and are called
|
|
as asynchronous functions).
|
|
|
|
Coroutines are almost as simple as synchronous code, but without the
|
|
expense of a thread. They also `make concurrency easier
|
|
<https://glyph.twistedmatrix.com/2014/02/unyielding.html>`_ to reason
|
|
about by reducing the number of places where a context switch can
|
|
happen.
|
|
|
|
Example::
|
|
|
|
from tornado import gen
|
|
|
|
@gen.coroutine
|
|
def fetch_coroutine(url):
|
|
http_client = AsyncHTTPClient()
|
|
response = yield http_client.fetch(url)
|
|
# In Python versions prior to 3.3, returning a value from
|
|
# a generator is not allowed and you must use
|
|
# raise gen.Return(response.body)
|
|
# instead.
|
|
return response.body
|
|
|
|
.. _native_coroutines:
|
|
|
|
Python 3.5: ``async`` and ``await``
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Python 3.5 introduces the ``async`` and ``await`` keywords (functions
|
|
using these keywords are also called "native coroutines"). Starting in
|
|
Tornado 4.3, you can use them in place of ``yield``-based coroutines.
|
|
Simply use ``async def foo()`` in place of a function definition with
|
|
the ``@gen.coroutine`` decorator, and ``await`` in place of yield. The
|
|
rest of this document still uses the ``yield`` style for compatibility
|
|
with older versions of Python, but ``async`` and ``await`` will run
|
|
faster when they are available::
|
|
|
|
async def fetch_coroutine(url):
|
|
http_client = AsyncHTTPClient()
|
|
response = await http_client.fetch(url)
|
|
return response.body
|
|
|
|
The ``await`` keyword is less versatile than the ``yield`` keyword.
|
|
For example, in a ``yield``-based coroutine you can yield a list of
|
|
``Futures``, while in a native coroutine you must wrap the list in
|
|
`tornado.gen.multi`. You can also use `tornado.gen.convert_yielded`
|
|
to convert anything that would work with ``yield`` into a form that
|
|
will work with ``await``.
|
|
|
|
While native coroutines are not visibly tied to a particular framework
|
|
(i.e. they do not use a decorator like `tornado.gen.coroutine` or
|
|
`asyncio.coroutine`), not all coroutines are compatible with each
|
|
other. There is a *coroutine runner* which is selected by the first
|
|
coroutine to be called, and then shared by all coroutines which are
|
|
called directly with ``await``. The Tornado coroutine runner is
|
|
designed to be versatile and accept awaitable objects from any
|
|
framework; other coroutine runners may be more limited (for example,
|
|
the ``asyncio`` coroutine runner does not accept coroutines from other
|
|
frameworks). For this reason, it is recommended to use the Tornado
|
|
coroutine runner for any application which combines multiple
|
|
frameworks. To call a coroutine using the Tornado runner from within a
|
|
coroutine that is already using the asyncio runner, use the
|
|
`tornado.platform.asyncio.to_asyncio_future` adapter.
|
|
|
|
|
|
How it works
|
|
~~~~~~~~~~~~
|
|
|
|
A function containing ``yield`` is a **generator**. All generators
|
|
are asynchronous; when called they return a generator object instead
|
|
of running to completion. The ``@gen.coroutine`` decorator
|
|
communicates with the generator via the ``yield`` expressions, and
|
|
with the coroutine's caller by returning a `.Future`.
|
|
|
|
Here is a simplified version of the coroutine decorator's inner loop::
|
|
|
|
# Simplified inner loop of tornado.gen.Runner
|
|
def run(self):
|
|
# send(x) makes the current yield return x.
|
|
# It returns when the next yield is reached
|
|
future = self.gen.send(self.next)
|
|
def callback(f):
|
|
self.next = f.result()
|
|
self.run()
|
|
future.add_done_callback(callback)
|
|
|
|
The decorator receives a `.Future` from the generator, waits (without
|
|
blocking) for that `.Future` to complete, then "unwraps" the `.Future`
|
|
and sends the result back into the generator as the result of the
|
|
``yield`` expression. Most asynchronous code never touches the `.Future`
|
|
class directly except to immediately pass the `.Future` returned by
|
|
an asynchronous function to a ``yield`` expression.
|
|
|
|
How to call a coroutine
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Coroutines do not raise exceptions in the normal way: any exception
|
|
they raise will be trapped in the `.Future` until it is yielded. This
|
|
means it is important to call coroutines in the right way, or you may
|
|
have errors that go unnoticed::
|
|
|
|
@gen.coroutine
|
|
def divide(x, y):
|
|
return x / y
|
|
|
|
def bad_call():
|
|
# This should raise a ZeroDivisionError, but it won't because
|
|
# the coroutine is called incorrectly.
|
|
divide(1, 0)
|
|
|
|
In nearly all cases, any function that calls a coroutine must be a
|
|
coroutine itself, and use the ``yield`` keyword in the call. When you
|
|
are overriding a method defined in a superclass, consult the
|
|
documentation to see if coroutines are allowed (the documentation
|
|
should say that the method "may be a coroutine" or "may return a
|
|
`.Future`")::
|
|
|
|
@gen.coroutine
|
|
def good_call():
|
|
# yield will unwrap the Future returned by divide() and raise
|
|
# the exception.
|
|
yield divide(1, 0)
|
|
|
|
Sometimes you may want to "fire and forget" a coroutine without waiting
|
|
for its result. In this case it is recommended to use `.IOLoop.spawn_callback`,
|
|
which makes the `.IOLoop` responsible for the call. If it fails,
|
|
the `.IOLoop` will log a stack trace::
|
|
|
|
# The IOLoop will catch the exception and print a stack trace in
|
|
# the logs. Note that this doesn't look like a normal call, since
|
|
# we pass the function object to be called by the IOLoop.
|
|
IOLoop.current().spawn_callback(divide, 1, 0)
|
|
|
|
Finally, at the top level of a program, *if the `.IOLoop` is not yet
|
|
running,* you can start the `.IOLoop`, run the coroutine, and then
|
|
stop the `.IOLoop` with the `.IOLoop.run_sync` method. This is often
|
|
used to start the ``main`` function of a batch-oriented program::
|
|
|
|
# run_sync() doesn't take arguments, so we must wrap the
|
|
# call in a lambda.
|
|
IOLoop.current().run_sync(lambda: divide(1, 0))
|
|
|
|
Coroutine patterns
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Interaction with callbacks
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
To interact with asynchronous code that uses callbacks instead of
|
|
`.Future`, wrap the call in a `.Task`. This will add the callback
|
|
argument for you and return a `.Future` which you can yield:
|
|
|
|
.. testcode::
|
|
|
|
@gen.coroutine
|
|
def call_task():
|
|
# Note that there are no parens on some_function.
|
|
# This will be translated by Task into
|
|
# some_function(other_args, callback=callback)
|
|
yield gen.Task(some_function, other_args)
|
|
|
|
.. testoutput::
|
|
:hide:
|
|
|
|
Calling blocking functions
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The simplest way to call a blocking function from a coroutine is to
|
|
use a `~concurrent.futures.ThreadPoolExecutor`, which returns
|
|
``Futures`` that are compatible with coroutines::
|
|
|
|
thread_pool = ThreadPoolExecutor(4)
|
|
|
|
@gen.coroutine
|
|
def call_blocking():
|
|
yield thread_pool.submit(blocking_func, args)
|
|
|
|
Parallelism
|
|
^^^^^^^^^^^
|
|
|
|
The coroutine decorator recognizes lists and dicts whose values are
|
|
``Futures``, and waits for all of those ``Futures`` in parallel:
|
|
|
|
.. testcode::
|
|
|
|
@gen.coroutine
|
|
def parallel_fetch(url1, url2):
|
|
resp1, resp2 = yield [http_client.fetch(url1),
|
|
http_client.fetch(url2)]
|
|
|
|
@gen.coroutine
|
|
def parallel_fetch_many(urls):
|
|
responses = yield [http_client.fetch(url) for url in urls]
|
|
# responses is a list of HTTPResponses in the same order
|
|
|
|
@gen.coroutine
|
|
def parallel_fetch_dict(urls):
|
|
responses = yield {url: http_client.fetch(url)
|
|
for url in urls}
|
|
# responses is a dict {url: HTTPResponse}
|
|
|
|
.. testoutput::
|
|
:hide:
|
|
|
|
Interleaving
|
|
^^^^^^^^^^^^
|
|
|
|
Sometimes it is useful to save a `.Future` instead of yielding it
|
|
immediately, so you can start another operation before waiting:
|
|
|
|
.. testcode::
|
|
|
|
@gen.coroutine
|
|
def get(self):
|
|
fetch_future = self.fetch_next_chunk()
|
|
while True:
|
|
chunk = yield fetch_future
|
|
if chunk is None: break
|
|
self.write(chunk)
|
|
fetch_future = self.fetch_next_chunk()
|
|
yield self.flush()
|
|
|
|
.. testoutput::
|
|
:hide:
|
|
|
|
Looping
|
|
^^^^^^^
|
|
|
|
Looping is tricky with coroutines since there is no way in Python
|
|
to ``yield`` on every iteration of a ``for`` or ``while`` loop and
|
|
capture the result of the yield. Instead, you'll need to separate
|
|
the loop condition from accessing the results, as in this example
|
|
from `Motor <http://motor.readthedocs.org/en/stable/>`_::
|
|
|
|
import motor
|
|
db = motor.MotorClient().test
|
|
|
|
@gen.coroutine
|
|
def loop_example(collection):
|
|
cursor = db.collection.find()
|
|
while (yield cursor.fetch_next):
|
|
doc = cursor.next_object()
|
|
|
|
Running in the background
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
`.PeriodicCallback` is not normally used with coroutines. Instead, a
|
|
coroutine can contain a ``while True:`` loop and use
|
|
`tornado.gen.sleep`::
|
|
|
|
@gen.coroutine
|
|
def minute_loop():
|
|
while True:
|
|
yield do_something()
|
|
yield gen.sleep(60)
|
|
|
|
# Coroutines that loop forever are generally started with
|
|
# spawn_callback().
|
|
IOLoop.current().spawn_callback(minute_loop)
|
|
|
|
Sometimes a more complicated loop may be desirable. For example, the
|
|
previous loop runs every ``60+N`` seconds, where ``N`` is the running
|
|
time of ``do_something()``. To run exactly every 60 seconds, use the
|
|
interleaving pattern from above::
|
|
|
|
@gen.coroutine
|
|
def minute_loop2():
|
|
while True:
|
|
nxt = gen.sleep(60) # Start the clock.
|
|
yield do_something() # Run while the clock is ticking.
|
|
yield nxt # Wait for the timer to run out.
|