On Mon, 2011-10-31 at 21:42 +1300, Greg Ewing wrote:
Thinking about how to support cofunctions iterating over generators that are themselves cofunctions and therefore suspendable, I've come to realise that cofunctionness and generatorness really need to be orthogonal concepts. As well as functions and cofunctions, we need generators and cogenerators.
And thinking about how to allow *that* while building on the yield-from mechanism, I found myself inventing what amounts to a complete parallel implementation of the iterator protocol, in which cogenerators create coiterators having a __conext__ method that raises CoStopIteration when finished, etc, etc... :-\
At which point I thought, well, why not forget about using the iterator protocol as a basis altogether, and design a new protocol specifically designed for the purpose?
About then I also remembered a thought I had in passing earlier, when Nick was talking about the fact that, when using yield-from, there is no object available that can hold a stack of suspended generators, so you end up traversing an ever-lengthening chain of generator frames as the call stack gets deeper.
I keep coming back to resumable exceptions as a way to suspend and resume, separate from the yield data path. Looking up previous discussions about it, there have been requests for them, but for the most part, those specific uses were better handled in other ways. There is also the problem that at a low level, they can be non-determinant as to where exactly the exception actually occurred. These negatives have pretty much killed any further discussion.
If we try to make all exceptions resumable, (either at the line they occurred or the line after), there are a lot of questions as to just how to make it work in a nice way. Enough so, it isn't worth doing.
But I think there is a way to look at them in a positive light. If we put some strict requirements on the idea.
1. Only have a *SINGLE* exception type as being resumable.
2. That exception should *NEVER* occur naturally.
3. Only allow continuing after it's *EXPLICITLY RAISED* by a raised statement.
All of the problem issues go away with those requirements in place, and you only have the issue of how to actually write the patch. Earlier discussions indicated, it might not be that hard to do.
Just like ValueError, a ResumableException would/should *never* occur naturally, so there is no issue about where it happened. So as long as they are limited to a single exception type, it may be doable in a clean way. Also they could be sub-classed so in effect offer a multi-channel way to easily control co-routines.
The problems with existing designs...
I've been playing around with co-function pipes where the data is pulled through. Once you figure out how they work they are fairly easy to design as they are just generators that are linked together in a chain.
At the source end is of course some sort of data source or generator, and each link operates on items as they a pulled by a next() call on the other end. In a single pipe design, the last item is also the consumer and pulls things through the pipe as it needs it.
But if you need to regulate the timing or run more than one pipe at a time, it requires adding an additional controller generator at the end that serves as part of the framework. The scheduler then does a next() call on the end generator, which causes an item to be pulled through the pipe, The consumer next to the end must push the data somewhere in that case.
This type of design has a major problem as the speed the pipe works is determined by how long it takes for data to go through it. Thats not good if we are trying to run many pipes at once.
Sense they can only suspend at yields, it requires sending scheduling data through the pipe along with (or instead of) the data and sort that back out at some point. Once we do that, our general purpose generators become tied to the framework. The dual iterator protocol is one way around that.
A trampoline can handle this because it sits between every co-function, so it can check the data for signal objects or types that can be handled outside of the coroutines. And then push back the data that it dosn't know how to handle.
That works, but it still requires additional overhead to check those messages. And trampolines work by yielding generators, so they do require a bit of studying to understand how they work before you use them.
How a ResumableException type would help...
With a resumable exception type, we create a data path outside of functions and generators that is easy to parse. So inside the coroutines we only need to add a "raise ResumableException" to transfer control to the scheduler. And then in the scheduler it catches the exception, handles any message it may have, and saves it in a stack. And it can then resume each coroutine by possibly doing a "continue ResumableException".
These could be easily sub-classed to create a scheduler ...
while 1: thread = stack.popleft() try: continue thread except AddThread as e: ... except RemoveThread as e: ... except Block as e: ... except Unblock as e: ... except ResumableException as e: thread = e stack.append(thread)
Where each of those is a sub-class of ResumableException.
Notice that, the scheduler is completely separate from the data path, it doesn't need to get the data, test it, and refeed it back in like a trampoline does. It also doesn't need any if-else structure, and doesn't need to access any methods at the python level if the "continue" can take the exception directly. So it should be fast.
A plain "raise ResumableExeption" could be auto continued if it isn't caught by default. That way you can write a generator that can work both in threads and by it self. (It's not an error.)
There would be no change or overloading of "yield" to make these work as threads. "Yield from" should work as it was described just fine and would be complementary to this. It just makes the whole idea of coroutine based lightweight threads a whole lot easier to use and understand.
I think this idea would also have uses in other asynchronous designs. But it definitely should be limited to a single exception type with the requirements I stated above.
However, with cofunctions there *is* a place where we could create such an object -- it could be done by costart(). We just need to allow it to be passed to the places where it's needed, and with a brand-new protocol we have the chance to do that.
I'll give a sketch of what such a protocol might be like in my next post.