Skip to content

Instantly share code, notes, and snippets.

@influentcoder
Created December 8, 2020 22:56
Show Gist options
  • Save influentcoder/c2eb3fff025a6cb3d35f4c2eca4f764c to your computer and use it in GitHub Desktop.
Save influentcoder/c2eb3fff025a6cb3d35f4c2eca4f764c to your computer and use it in GitHub Desktop.
Greenlet Example
"""
Install greenlet before running this: pip install greenlet
"""
from greenlet import greenlet
class GUIFramework:
"""
This class simulates a GUI framework, that has its own main loop.
For simplicity, the loop is just waiting for user input from stdin, and when a key
is pressed, a user-defined callback is invoked.
A typical GUI framework would allow a user to define a certain number of callbacks,
and then the application is started by running the "main" function of the framework
- here "mainloop()".
"""
# "on_key_press_callback" is the callback that the user of the GUI framework will
# provide to handle key presses from the user in the GUI.
def __init__(self, on_key_press_callback):
self.callback = on_key_press_callback
def mainloop(self):
"""
Main loop of the GUI, we just pretend that the user types some commands.
"""
# The user types "hello"
for c in 'hello\n':
self.callback(c)
# The user types "quit"
for c in 'quit\n':
self.callback(c)
# The user responds to the prompt with 'y'
self.callback('y')
def on_key_press(self, key):
self.callback(key)
def process_commands():
"""
User-defined function that processes commands entered by the user and runs
application logic. Should be decoupled from the GUI framework, from the way that
commands are received (whether they are from a GUI, from stdin directly etc).
"""
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)
def process_command(line):
"""Main application logic"""
print("Processing: " + line)
def echo_user_input(user_input):
print(' <<< ' + user_input.strip())
return user_input
def read_next_char():
res = main_greenlet.switch("blocking here")
return res
def on_key_press_callback(key):
"""Callback invoked by the GUI framework, which gives back the control to the
g_processor greenlet."""
g_processor.switch(key)
g_processor = greenlet(process_commands)
main_greenlet = greenlet.getcurrent()
if __name__ == "__main__":
# Here we switch to the g_processor greenlet. What happens is that the
# "process_commands()" function is executed in a greelet. If you place a debugger in
# it and inspect the callstack, it will be empty, similarly as if we were running
# this in a thread that has its own stack trace. But here, he are not using threads,
# everything is synchronous, but greenlets gives us the illusion that we are running
# something a thread.
# The "process_commands()" function will continue until "read_next_char()" in
# invoked, as "read_next_char()" passes back the control to the main / parent
# greenlet i.e. here.
g_processor.switch()
# Here the callstack is:
# * <module> (test.py)
# The callstack of the g_processor greenlet is:
# * process_commands()
# * read_next_char()
# So the next time we call "g_processor.switch()", the execution will continue where
# it left off in "read_next_char()".
# We start the mainloop of the GUI framework.
# When the user presses a key, the "on_key_press_callback()" will be called, and it
# will switch to the "g_processor" greenlet in "read_next_char()", by giving the key
# that was entered. Then "process_commands()" will continue and again call
# "read_next_char()" etc.
# This is best visualised by running a debugger step by step.
gui = GUIFramework(on_key_press_callback)
gui.mainloop()
# Final output:
# <<< hello
# Processing: hello
# <<< quit
# Are you sure?
# <<< y
# (Exiting loop.)
# The problem we are solving here is how to integrate two loops together (the one
# from the GUI framework and the one in the user application).
# This could have been achieved with threads and a synchronized data structure like
# a queue - the GUI framework would enqueue key presses from the user, and the
# application would run in a separate thread and consume the keys from the queue.
# We would have to handle scenarios such as what happens if the processing is slow
# and the queue becomes full, and other related async issues. We can see that this
# implementation is much cleaner.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment