Channels ▼
RSS

Tools

The New asyncio Module in Python 3.4: Event Loops


Python 3.4 added a new asynchronous I/O module named asyncio (formerly known as Tulip). The asyncio module provides a new infrastructure with a plugabble event loop, transport and protocol abstractions, a Future class (adapted for use within the event loop), coroutines, tasks, threadpool management, and synchronization primitives to simplify coding concurrent code. In this overview of asyncio, I provide a brief introduction to the main components of the module and a few simple sample applications that work with some of the event loop functions.

Main Components of the New asyncio Module

The asyncio module is a game-changer for I/O management in Python because it is included in the Python standard library and provides a modern replacement for the ancient, ever-problematic, and non-extensible asyncore module. It is an extremely useful module for developers who need to work with concurrent code and I/O without losing all the advantages and simplicity of sequential code. It makes it possible to work with asynchronous I/O as if you were working with sequential, synchronous I/O. The module proposes a new way of writing callbacks without actually using callbacks. However, it is a brand new module that developers will need to learn from scratch.

The mission to add asynchronous I/O to the Python standard library started in 2012. Python's Benevolent Dictator for Life, Guido van Rossum, used simple words to explain the decision to build the asyncio module: "I'm not trying to reinvent the wheel. I'm trying to build a good one."

The asyncio module comprises the following principal components:

  • Event loop. The event loop multiplexes I/O, serializes event handling, and works with a policy pattern to be extremely flexible for custom platforms and frameworks. One of the main goals of asyncio is to provide interoperability with other frameworks. For example, Tornado, Twisted, and GEvent can work with and be built on top of asyncio. In fact, the event loop has influence from both Tornado and Twisted. In addition, asyncio chooses the best I/O mechanism for each platform. UNIX and Linux work with selectors, while Windows-based systems work with IOCP (short for I/O completion ports).
  • Futures. These are abstractions for values that will be produced later. An exception is also considered a result. The asyncio.futures.Future class is similar to the PEP 3148 Future introduced in Python 3.2; that is, the concurrent.futures.Future class. However, in this case, Future is adapted for its usage with coroutines and it is a different class with a different API. The asyncio module does not use the existing concurrent.futures.Future class because it was designed to work with threads. The module instead encourages the use of yield from within coroutines to easily block a current task waiting for the results without blocking your application. Your coroutine blocks; that is, your coroutine is suspended until there is a result, but your event loop is not blocked. If the same event loop has other managed tasks, they might run. Your suspended coroutine resumes when there is a result and you can work with the code in the same way you work with serial code. You can read the code without considering the existence of yield from. When you use yield from with a function returning a yield from, you can forget about the specific details of the Future implementation and its specific API. If an exception occurs, it will be raised just as when you are calling a function that doesn't return a Future and runs with a sequential execution. So, you work with asynchronous code in the same way you would work with synchronous code, except that you add yield from. If you have experience with Twisted, you will notice some influence from its @defer.inlineCallbacks decorator. If you prefer working with callbacks, you can use the add_done_callback method to register a callback to be run when the Future is done.
  • Coroutines. These are generator functions that can receive values and must be decorated with @coroutine (@asyncio.coroutine). The @coroutine decorator indicates that you are using yield from to pass each Future. The decorator makes sure that whenever you read the code, you know that the code is using the asynchronous paradigm. You must use yield from within these generator functions. If you are familiar with C# 5.0, you will notice that @coroutine and yield from have exactly the same effect as the async and await keywords in C#.
  • Tasks. Each task is a coroutine wrapped inside a Future and runs as long as the event loop runs. The asyncio.Task class is a subclass of asyncio.Future. As you might guess, tasks also work with yield from.
  • Transports. They represent connections such as sockets and pipes.
  • Protocols. They represent applications such as an HTTP server, SMTP, and FTP.

If you want to use the features included in asyncio in previous Python versions, consider Trollius, a backport of asyncio for Python 2.6 or later. Trollius uses a slightly different syntax than asyncio, but provides you most of the features of the asyncio module.

Working with Some Event Loop Methods

The event loop is the central execution device included in the asyncio module. The easiest way to understand basic usage of the even loop is by running a short and simple piece of code that uses it. The following lines show a coroutine, just_print_messages, that executes a loop forever in which it prints a message and then sleeps for three seconds.

import asyncio

@asyncio.coroutine
def just_print_messages():
    """
    This method prints a message and then sleeps for three seconds
    """
    while True:
        print('Just printing a new message every three seconds')
        yield from asyncio.sleep(3)

def main():
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(just_print_messages())
    finally:
        loop.close()

if __name__ == '__main__':
    main()

The main method creates an event loop (loop) with the default policy by calling the asyncio.get_envent_loop method. Then the code calls the loop.run_until_complete method with just_print_message() as an argument. The run_until_complete method runs until the Future received as an argument is done. In this case, the argument is a coroutine, so it is wrapped in a Task. Notice that the just_print_message method is decorated with @async.coroutine and executes a loop forever that prints a message and then uses yield from to block the coroutine until the asyncio.sleep coroutine finishes its execution, without blocking the event loop.

The asyncio.sleep method is a coroutine that completes after the delay time in seconds received as an argument. The following lines show the source code for this method, which schedules a call to happen after the specified delay in seconds, then uses a yield from to wait for the Future that is scheduled to execute and return its result. I think that the source code of this implementation of sleep provides a clear illustration of how the event loop is the main component of the asyncio module — there are many coroutines under the hood working with the combination of Futures, tasks, and scheduled executions coordinated by the event loop.

@coroutine
def sleep(delay, result=None, *, loop=None):
    future = futures.Future(loop=loop)
    h = future._loop.call_later(delay, future.set_result, result)
    try:
        return (yield from future)
    finally:
        h.cancel()

As I explained earlier, if you like callbacks, you can work with them instead of combining yield from with Futures or coroutines. I think it is a good idea to avoid callbacks as much as possible, but in the event you cannot avoid them, the following lines show the example that prints a message every three seconds rewritten with callbacks.

import asyncio

def just_print_messages(loop):
    """
    This method prints a message and schedules another call to itself after three seconds
    """
    print('Just printing a new message every three seconds')
    loop.call_later(3, just_print_messages, loop)

def main():
    loop = asyncio.get_event_loop()
    try:
        loop.call_soon(just_print_messages, loop)
        loop.run_forever()
    finally:
        loop.close()

if __name__ == '__main__':
    main()

The main method creates an event loop (loop) with the default policy by calling the asyncio.get_envent_loop method. Then, the code calls the loop.call_soon method with just_print_message and loop as the arguments. Notice that in this case, just_print_message is not decorated with @async.coroutine. The call_soon method arranges for the callback received as an argument to be called as soon as possible. The method operates as a FIFO queue and the arguments after the callback are passed to the callback when it is called. Thus, just_print_messages will be called with loop as its argument. The code calls loop.run_forever() to run the event loop until the stop() method is called.

When the event loop executes just_print_messages with the event loop as an argument, the method prints a message and calls the loop.call_later method, which arranges for a callback to be called at a given time. The first argument specifies a delay of three seconds before executing the callback. The second argument specifies the current method, just_print_messages, as the callback to be executed. The arguments after the callback are passed to the callback when it is called. In this case, the third argument is loop, so just_print_messages will be called with loop as its argument. Then, the method ends its execution and the event loop will start the execution of the method again after three seconds have elapsed from the time loop.call_later was called.

Both loop.call_soon and loop.call_later return an asyncio.Handle instance — a callback wrapper object. In the previous example, the code didn't take into account the returned asyncio.Handle instance. The following lines show a new version that assigns the asyncio.Handle instance returned by loop.call_later to handle. Then, the code within the just_print_messages method makes another call to loop.call_later to schedule a call to the cancel_call method one second later, with handle as one of its arguments. Thus, one second later, the event loop calls cancel_call and this method calls the cancel() method for the handle received as an argument. As a result, the event loop cancels the previously scheduled call to the just_print_messages method. This time, when you execute the sample code, the message appears just once and the other scheduled calls are always canceled by using the asyncio.Handle instance.

import asyncio

def cancel_call(handle):
    """
    This method cancels the call wrapped in the asyncio.Handle received as an argument
    """
    handle.cancel()

def just_print_messages(loop):
    """
    This method prints a message and schedules another call to itself after three seconds
    Then, the method schedules a callback that cancels the previously scheduled callback
    """
    print('Just printing a new message every three seconds')
    handle = loop.call_later(3, just_print_messages, loop)
    loop.call_later(1, cancel_call, handle)

def main():
    loop = asyncio.get_event_loop()
    try:
        loop.call_soon(just_print_messages, loop)
        loop.run_forever()
    finally:
        loop.close()

if __name__ == '__main__':
    main()

Conclusion

These examples illustrate the use of the asyncio event loop functions that register, execute, and cancel calls and delayed calls. Obviously, the examples are extremely simple, and I focused on the use of only a few basic functions just to give you an idea of the way the event loop works. It can be a bit difficult to understand everything about coroutines, features, and callback style if you work with protocols and transports at the same time. In the next article in this series, I'll move on to discussing the protocols, transports, and how to work with many of the features introduced by the new asyncio module.


Gastón Hillar is a senior contributing editor at Dr. Dobb's.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video