Motivation: Treating an Asynchronous GUI Like a Synchronous Loop#

In this document, we’ll demonstrate how greenlet can be used to connect synchronous and asynchronous operations, without introducing any additional threads or race conditions. We’ll use the example of transforming a “pull”-based console application into an asynchronous “push”-based GUI application while still maintaining the simple pull-based structure.

Similar techniques work with XML expat parsers; in general, it can be framework that issues asynchronous callbacks.

A Simple Terminal App#

Let’s consider a system controlled by a terminal-like console, where the user types commands. Assume that the input comes character by character. In such a system, there will typically be a loop like the following one:

>>> def echo_user_input(user_input):
...     print('    <<< ' + user_input.strip())
...     return user_input
>>> def process_commands():
...    while True:
...        line = ''
...        while not line.endswith('\n'):
...            line += read_next_char()
...        echo_user_input(line)
...        if line == 'quit\n':
...            print("Are you sure?")
...            if echo_user_input(read_next_char()) != 'y':
...                continue    # ignore the command
...            print("(Exiting loop.)")
...            break # stop the command loop
...        process_command(line)

Here, we have an infinite loop. The job of the loop is to read characters that the user types, accumulate that into a command line, and then execute the command. The heart of the loop is around read_next_char():

>>> def read_next_char():
...    """
...    Called from `process_commands`;
...    blocks until a character has been typed and returns it.
...    """

This function might be implemented by simply reading from sys.stdin, or by something more complex such as curses.window.getch(), but in any case, it doesn’t return until a key has been read from the user.

Competing Event Loops#

Now assume that you want to plug this program into a GUI. Most GUI toolkits are event-based. Internally, they run their own infinite loop much like the one we wrote above, invoking a call-back for each character the user presses (event_keydown(key)).

>>> def event_keydown(key):
...    "Called by the event system *asynchronously*."

In this setting, it is difficult to implement the read_next_char() function needed by the code above. We have two incompatible functions. First, there’s the function the GUI will call asynchronously to notify about an event; it’s important to stress that we’re not in control of when this function is called—in fact, our code isn’t in the call stack at all, the GUI’s loop is the only thing running. But that doesn’t fit with our second function, read_next_char() which itself is supposed to be blocking and called from the middle of its own loop.

How can we fit this asynchronous delivery mechanism together with our synchronous, blocking function that reads the next character the user types?

Enter greenlets: Dual Infinite Loops#

You might consider doing that with threads [1], but that can get complicated rather quickly. greenlets are an alternate solution that don’t have the related locking and other problems threads introduce.

By introducing a greenlet to run process_commands, and having it communicate with the greenlet running the GUI event loop, we can effectively have a single thread be in the middle of two infinite loops at once and switch between them as desired. Pretty cool.

It’s even cooler when you consider that the GUI’s loop is likely to be implemented in C, not Python, so we’ll be switching between infinite loops both in native code and in the Python interpreter.

First, let’s create a greenlet to run the process_commands function (note that we’re not starting it just yet, only defining it).

>>> from greenlet import greenlet
>>> g_processor = greenlet(process_commands)

Now, we need to arrange for the communication between the GUI’s event loop and its callback event_keydown (running in the implicit main greenlet) and this new greenlet. The changes to event_keydown are pretty simple: just send the key the GUI gives us into the loop that process_commands is in using greenlet.switch().

>>> main_greenlet = greenlet.getcurrent()
>>> def event_keydown(key): # running in main_greenlet
...     # jump into g_processor, sending it the key
...     g_processor.switch(key)

The other side of the coin is to define read_next_char to accept this key event. We do this by letting the main greenlet run the GUI loop until the GUI loop jumps back to is from event_keydown:

>>> def read_next_char(): # running in g_processor
...     # jump to the main greenlet, where the GUI event
...     # loop is running, and wait for the next key
...     next_char = main_greenlet.switch('blocking in read_next_char')
...     return next_char

Having defined both functions, we can start the process_commands greenlet, which will make it to read_next_char() and immediately switch back to the main greenlet:

>>> g_processor.switch()
'blocking in read_next_char'

Now we can hand control over to the main event loop of the GUI. Of course, in documentation we don’t have a GUI, so we’ll fake one that feeds keys to event_keydown; for demonstration purposes we’ll also fake a process_command function that just prints the line it got.

>>> def process_command(line):
...     print('(Processing command: ' + line.strip() + ')')

>>> def gui_mainloop():
...    # The user types "hello"
...    for c in 'hello\n':
...        event_keydown(c)
...    # The user types "quit"
...    for c in 'quit\n':
...        event_keydown(c)
...    # The user responds to the prompt with 'y'
...    event_keydown('y')

>>> gui_mainloop()
    <<< hello
(Processing command: hello)
    <<< quit
Are you sure?
    <<< y
(Exiting loop.)
>>> g_processor.dead
True

In this example, the execution flow is: when read_next_char() is called, it is part of the g_processor greenlet, so when it switches to its parent greenlet, it resumes execution in the top-level main loop (the GUI). When the GUI calls event_keydown(), it switches to g_processor, which means that the execution jumps back wherever it was suspended in that greenlet—in this case, to the switch() instruction in read_next_char()—and the key argument in event_keydown() is passed as the return value of the switch() in read_next_char().

Note that read_next_char() will be suspended and resumed with its call stack preserved, so that it will itself return to different positions in process_commands() depending on where it was originally called from. This allows the logic of the program to be kept in a nice control-flow way; we don’t have to completely rewrite process_commands() to turn it into a state machine.

Further Reading#

Continue reading with greenlet Concepts.

Curious how execution resumed in the main greenlet after process_commands exited its loop (and never explicitly switched back to the main greenlet)? Read about Greenlet Parents.

Footnotes