Search code examples
pythontwisted

Cooperation in Twisted - yield the execution context


Below is my testbed

from twisted.internet import defer, task


@defer.inlineCallbacks
def foo():
    yield
    print 'after yield in foo'


@defer.inlineCallbacks
def main(reactor):
    d = foo()
    yield
    print 'after yield in main'
    yield d


task.react(main)

I exepect that the yield statement will make the function to "yield the exeuction context" (whatever that means in Twisted) and let another deferred to take over the execution. For that particular example I expect that main() starts the execution, calls foo(), which being wrapped with inlineCallbacks is turned into a deferred, then yields the execution letting foo() to eventually start. Then foo() in its turn yields the execution too, so ultimately the order of printed lines should be

after yield in main
after yield in foo

For some reason the output is

after yield in foo
after yield in main

What is the right way to implement cooperative multi-tasking in Twisted and let the execution context go to another deferred in line?


Solution

  • I exepect that the yield statement will make the function to "yield the exeuction context" (whatever that means in Twisted) and let another deferred to take over the execution.

    From your observations, I take it you have been disabused of this expectation by now. To be perfectly clear, this is not what yield does in a generator function decorated by inlineCallbacks.

    What yield does in such a function is pass a value out to a trampoline. If the value is a Deferred, the trampoline suspends execution of the generator until the Deferred fires. If the value is not a Deferred, or when the Deferred fires with a value, the generator is resumed with that value sent in to it.

    So, since yield is the same as yield None and None is not a Deferred, these yield expressions are just an expensive way to say None.

    This also gives you some idea of how to achieve what you say you're after. To suspend execution, yield a Deferred that does not fire until you want execution to resume.

    What is the right way to implement cooperative multi-tasking in Twisted and let the execution context go to another deferred in line?

    There are many possibly correct ways to do this. The specifics depend more on your specific application requirements.

    Unfortunately, there are some easily explained cheesy answers which might seem correct but which will probably ultimately lead to poor performance and costly maintenance. For example, you can suspend for "a reactor iteration" (to the extent such a thing exists, which may be less so than you believe). To do this, yield twisted.internet.task.deferLater(reactor, 0.0, lambda: None). This expression makes a Deferred that fires no sooner than "zero seconds" from now but also with the constraint that it doesn't fire right now.

    However, this lets the whole reactor implementation have a chance at doing work before your generator is resumed - even if there is no other work to do. Thus, you're paying a large CPU cost for the possibility of being cooperative. Furthermore, it makes the function difficult to test by introducing time-based scheduling interactions.

    An alternative that Twisted offers to try to cope with some of these difficulties is twisted.internet.task.cooperate which drops inlineCallbacks in favor of bare generators. These actually do use yield to offer suspend-points but do so in a way that doesn't give the whole reactor a chance to run before resuming. This addresses some CPU concerns and possibly some maintenance difficulties, though ultimately it still introduces some time-based dependencies. However, it is at least possible to operate on the bare generators without involving cooperate which mitigates this difficulty to some extent (compare this with inlineCallbacks where idiomatically written code destroys the underlying generator and only offers a function that returns a Deferred up for testing).