A “greenlet” is a small independent pseudo-thread. Think about it as a small stack of frames; the outermost (bottom) frame is the initial function you called, and the innermost frame is the one in which the greenlet is currently paused.
In code, greenlets are represented by objects of class
greenlet. These objects have a few defined attributes, and
also have a
__dict__, allowing for arbitrary user-defined
Attribute names beginning with
gr_ are reserved for this
You work with greenlets by creating a number of such stacks and
jumping execution between them. Jumps are never implicit: a greenlet
must choose to jump to another greenlet, which will cause the former
to suspend and the latter to resume where it was suspended. Jumping
between greenlets is called “switching”. Similarly to
generator.send(val), switching may pass objects between greenlets.
The greenlet Lifecycle¶
- Details And Examples
- Where does execution go when a greenlet dies?
When you create a greenlet, it gets an initially empty stack; when you first switch to it, it starts to run a specified function, which may call other functions, switch out of the greenlet, etc. When eventually the outermost function finishes its execution, the greenlet’s stack becomes empty again and the greenlet is “dead”. Greenlets can also die of an uncaught exception, or be garbage collected (which raises an exception).
Let’s quickly pull together an example demonstrating those concepts before continuing with a few more concepts.
>>> from greenlet import greenlet >>> def test1(): ... print("[gr1] main -> test1") ... gr2.switch() ... print("[gr1] test1 <- test2") ... return 'test1 done' >>> def test2(): ... print("[gr2] test1 -> test2") ... gr1.switch() ... print("This is never printed.") >>> gr1 = greenlet(test1) >>> gr2 = greenlet(test2) >>> gr1.switch() [gr1] main -> test1 [gr2] test1 -> test2 [gr1] test1 <- test2 'test1 done' >>> gr1.dead True >>> gr2.dead False
gr1.switch() jumps to
test1, which prints that, jumps
test2, and prints that, jumps back into
test1, prints that;
test1 finishes and
gr1 dies. At this point, the
execution comes back to the original
gr1.switch() call, which
returns the value that
test1 returned. Note that
never switched back to and so doesn’t print its final line; it is also
Having seen that, we can continue with a few more concepts.
The Current greenlet¶
The greenlet that is actively running code is called the “current
greenlet object representing the current
greenlet can be obtained by calling
getcurrent(). (Note that
this could be a subclass.)
As long as a greenlet is running, no other greenlet can be running. Execution must be explicitly transferred by switching to a different greenlet.
The Main greenlet¶
Initially, there is one greenlet that you don’t have to create: the main greenlet. This is the only greenlet that can ever have a parent of None. The main greenlet can never be dead. This is true for every thread in a process.
>>> from greenlet import getcurrent >>> def am_i_main(): ... current = getcurrent() ... return current.parent is None >>> am_i_main() True >>> glet = greenlet(am_i_main) >>> glet.switch() False
Every greenlet, except the main greenlet, has a “parent” greenlet. The parent greenlet defaults to being the one in which the greenlet was created (this can be changed at any time). In this way, greenlets are organized in a tree. Top-level code that doesn’t run in a user-created greenlet runs in the implicit main greenlet, which is the root of the tree.
The parent is where execution continues when a greenlet dies, whether by explicitly returning from its function, “falling off the end” of its function, or by raising an uncaught exception.
In the above example, both
gr2 have the main greenlet
as a parent. Whenever one of them dies, the execution comes back to
Uncaught Exceptions are Raised In the Parent¶
Uncaught exceptions are propagated into the parent, too. For example, if
test2() contained a typo, it would generate a
gr2, and the exception would go back directly into “main”.
The traceback would show
test2, but not
test1. Remember, switches are not
calls, but transfer of execution between parallel “stack containers”, and
the “parent” defines which stack logically comes “below” the current one.
>>> def test2(): ... print(this_should_be_a_name_error) >>> gr1 = greenlet(test1) >>> gr2 = greenlet(test2) >>> gr1.switch() Traceback (most recent call last): ... File "<doctest default>", line 1, in <module> gr1.switch() File "<doctest default>", line 2, in test2 print(this_should_be_a_name_error) NameError: name 'this_should_be_a_name_error' is not defined