 What is Stackless Python? If you've heard of Stackless Python, you usually think of it associated with lightweight threads and usually very big online games, i.e. Eve. But in reality, the reality is Stackless Python is a super set of standard Python and we'll refer to standard, in this talk we're often going to refer to standard Python as C-Python and I'll explain that a little later. The language crew is created by Christian Tizmer and again, if you've heard of Stackless Python, you also, you know people tend to associate it with being able to recurse infinitely. That's a bit of an abyssinomer, but there's far more to Stackless Python than that. What's the motivation for my talk? The motivation for my talk was listening to other talks. More specifically, watching the Joe Gagario lack of design patterns in Python. There's what, it's a nice talk. There's one part in the talk where Gagario is, he shows a Python based prime number C of using a package called PyCSP. CSP stands for communicating sequential processes. That was created by Tony Hoare. Anyhow, what Gagario is trying to illustrate is a use of a high level concurrency construct, in this case, a channel. A part of this argument is threads are way too low level. The problem is, when you, when he runs the example and also I ran the example, after about a relatively small number, 2,609, the program conks out and you notice here, thread 381, can't start new thread. Well, there may be a number of reasons why that's happening, but it's creating a lot of threads. Reason, the SIV algorithm creates, basically creates a thread for each prime and CSP is using OS threads. So it runs out of threads. And Gagario, you know, he suggests that greenlets could be used. And also, in all honesty, when he's saying he's playing around with PyCSI, he's trying to look for something that could be used with C Python. Anyhow, the bigger picture of his talk is if you have to implement a design pattern, consider adding features to the language. And also he says that concurrency problems or patterns are a rich area. You know, I'm listening to all this. I'm saying, yeah, yeah, you know, I can buy that. I can live with that. Now, I guess, and I'm saying, you know, again, you know, this is a good talk, but, you know, it would have been nicer if you use Stackless Python as an example. After all, Stackless Python has all those language feature goodies, such as coroutines and channels. And Stackless Python implements much of a concurrency design pattern called Active Object. If you've heard of Active Object, it's also AKA Actor. And its intent is to decouple method invocation from method execution. Okay, I'm going to explain this. Basically, what Active Object tries to do is in the same object, it tries to encapsulate the data and the thread that uses it. And what you do is you have message passing through these actors. And what that basically does is it's simple, okay, it decouples the system. And also it makes things like synchronization simpler because it's basically one thread accesses the data. You don't have, so you don't really have things like locks. And the prominent features, and the prominent features of this design pattern is you have a scheduler, you have queues, and you have message passing. There's more features to it. But maybe I should have drawn a diagram for Active Object. But anyhow, the last draw was I'm listening to the talk. The talk is over, and there's this audience member that comes up, and he says, have you played with Stackless Python in its approach to channels and sub-threads? Gaguro? No. Audience member, well, you know, he's stunned. He says, okay, if you're interested, it has a very similar approach. And my reaction to that was, argh! There's other things I'd like to say, but I think this is sort of like a family show. And anyhow, so and one other thing, I hope I'm pronouncing a poor man's name, right, Gaguro, or is it Gagario? Pardon me? Gagario? Gagario, okay. Maybe we should just call them Joe. Hey, aren't all us Pythonistas on a first-name basis? But anyway, just a little note. Gaguro, what he does is he takes he himself, with CSP, he's copying the example that Robert Pike has in his advanced concurrency talk, except he does it in news squeak. What I did was I take the Pike program, and I basically translate it into Stackless Python, and it basically looks like this. Don't worry if you don't understand it right now, but you're going to see, I'm going to explain these little features of the language. It's very short, and there was one other really neat thing that I wasn't aware of. This is a channel, but that's a channel, but she knows that it's being used as an iterator. Very nice. And again, Pike in his talk about the sieve is saying, well, the new squeak version is really, really nice. It's hard to get it smaller. This is slightly smaller. And I have a feeling maybe a generator with a list comprehension could be a bit tighter, but you know, hey. So some comments. As I said, the Stackless ver examples is almost as terse as Pike's example, and you can look for his example. This application has no problem. Okay, this problem has no problem finding 9,973. When Gregorio was trying to run his, he's trying to get up to 10,000. I left this thing on at night, and I decided to stop it after I got to that number. And it didn't even hit 90% CPU doing it. I should have taken more measurements. But anyway, given all this, this is my, this is my basically my case. My case is Stackless Python strength is not only its ability to create lightweight threads. It's also that it has a rather high concurrency model. The fact that it gives you stuff like activity object, it's built in the schedulers built in the, the communications mechanisms are all built in. It means that you're focusing more on algorithms. You're not focusing on nitty gritty. And isn't that what Pythonic is about? And I'll, I'll throw in one other thing. I was watching the David Bezley talk on a curious course on coroutines. It's a great, never great talk. And at the end of the talk, he's basically showing how to use generators as coroutines. And a lot of his talk is just showing techniques like trampolining, building up schedulers. And that towards the end, he says, well, my jury's hung on this. I don't really know if it's worthwhile building an operating system. And if you take a look at his talk, a large part of the machinery is spent building up the scheduler. Stackless gives it to you for free. So anyway, let's shift some gear, let's shift gears here. Getting Stackless Python. Basically a few ways of getting it. Check it out via subversion. You get some tar balls and you can see it's fairly up to date. There's some binary installers for Windows and a Mac. And yeah, let's thank CCP Game for, you know, being timely and updating Stackless Python. Also, if you want more information, you can go to the Stackless wiki. And also I would recommend you go to the mailing list. There you can see me just pestering folks with questions. Anyway, we start up, we start up Python. I'm using 2.6.2. I haven't quite gone up the three yet. And you see we import the Stackless module. And if we do a DIR, we can see most of the components of Stackless. Very few of these are used by an application programmer. If you're building some tools, there's a few more that you may use. Again, and if you do help, you will see the list of, and sometimes this is very useful because of the lack of documentation. Okay, Stackless and Stackless, what makes Stackless Python Stackless? Well, I'm not an expert in the internals of Stackless Python, but basically it's that, you know, Python, Python frame, it's simply Python frames are stored on the heap rather than on the C stack. So things like when you hear people talk about infinite recursion, well, Stack overflows just manifest themselves as memory errors. The fact that they've moved, they've moved the frames on the heap, enable a few things. Enables the creation, easy creation of coroutines, and pickling of execution state. So basically what you could, by pickling of execution state, basically what you could do with a Stackless Python program is, you can halt it in its tracks, serialize it to say a disk, bring it to another machine, and reactivate it, and it'll pick up from where it left off. It's a very neat feature. So anyway, let's get on to Stackless Python's model. There's very, there's not very much to it. There's basically tasklets for concurrency, the scheduler, and channels. That's it. So let's, I'll explain the scheduler. I'm going to, by, I guess most of you've heard, like, in Unix talk or user space and kernel space. I'll assume you've heard that. The scheduler lives in user space, or the Stackless scheduler lives in user space. It's what performs context switches on behalf of the programmer. I'll sort of explain this towards the end of the talk. And tasklets basically have three states. It's either the tasklet that's currently running, it could be run, or it's blocked. And here's the scheduling algorithm. It uses a round-robin algorithm. Tasklets that can run are stored on a runnable list. Runnable is a doubly, you know, if you're interested, it's a doubly-linked list. Nothing unique about that is basically, anyway. And blocked tasklets are stored on a list associated with a tasklet's channel. If you call Schedule Get Current, it indicates the currently running tasklet ahead. Normally, I get someone to draw my diagrams. Also, I don't know what somebody got lost in translation. I'm very sorry about that. I'll try to fix this up. But anyway, this is essentially the runnable list. And what you see is a tasklet will have channels associated with it. And what this means is tasklet D and E in this thing, they're part of the blocked tasklets. And when we get to channel priorities, this will make a bit more sense. Essentially, this is a way stack... This is the high-level model of the stackless scheduler. Again, in a few days, I'll just fix all this up. So anyway, let's go on to stack... To talking... Yes, thanks. To multi... A stackless basically has two multitasking modes. One of them is pre-emptive multitasking, which is more or less the way most OS schedulers work. However, here, the tasklets are giving a CPU quota based on instruction ticks. The second mode is cooperative multitasking, where the tasklets explicitly yield control to the scheduler. We're going to focus on cooperative. It's a far simpler model than when used preemptive. You have to do far more. And also, you have to start doing things like using critical... Something called atomic, which sort of sets up a critical section. And that starts to get a bit messy. But so we stick to the cooperative scheduling. It's a far simpler model. It's ideal for IO bound applications. And after all, I guess the main problem we're dealing with is handling multiple current events, as opposed to, let's say, doing heavy computations. Yes? Okay, because let's say with IO bound, you naturally know where the tasklet is going to yield, where you want to suspend it, and when you want to resume it. That's why I would say that for IO bound, it's much more applicable, as opposed to something that's compute bound. Because then you have to arbitrarily put in yields. Okay, tasklets. The tasklet class represents a threat of execution. This is where the work gets done. Each tasklet has its own stack frame. And these tasklets live in user space. They don't belong to the operating system. The underlying operating system is ignorant of tasklets. Anyway, tasklets have very low overhead. I've heard a quote that it's roughly 400 bytes per tasklet. And again, the context switching is very fast. Soft switching is about 100 times faster than an OS thread. Hard switching is 10 times faster. And basically, again, this is a bit esoteric even for me. The difference between soft scheduling and hard scheduling has to do with C extension. Sometimes you're dealing with a C extension and you have stuff on the C stack and they have to handle it differently. Beyond that, I'm not quite sure of all the nitty-gritty details. Okay, let's start looking at a stackless Python program, although we already looked at one. Very simple. This is the function. We bind it to a tasklet. We're not passing it any parameters. And we tell the stackless scheduler to run and everything starts and we get hello world. Not too exciting. A more complex example is with two tasks. With two, we create two tasklets. And basically what we're doing is we're going through a loop and we're calling schedule. And when we call schedule, what we're doing is we're yielding control to the scheduler and it will run the next stack, the next tasklet that can run. So basically you're getting these numbers interwoven. Again, not very exciting. We get to channels. Channels are really the heart of stackless Python's concurrency model. Channels sort of incorporate both communications and synchronization. They do them both. Basically, channels come from NewSqueak and Limbo. Both of these are associated with Plan 9 from Bell Labs. I think it's a part of Plan 9 and Inferno. So a part of that is NewSqueak and Limbo. These guys are really, if you look at that work, it's a really important source of idioms. You get ideas of how to program stackless Python from looking at the way they use channels. A lot of the tricks are almost directly applicable. So again, channels don't exist. Actually, this is a bit of a misnomer. I'm going to say they're associated with a tasklet, but maybe we should just strike that out. It's a bit of a misnomer, they're bi-directional. You could either send on a channel and receive on a channel. They're unbuffered. And references, including channels and exceptions, can be passed in a channel. This last part's very important. At the heart of channels is synchronous communications. Channels are synchronous. If there's no receiver, the sender will block. If there's no sender, the receiver will block. So basically, it's rendezvous semantics. Also, another neat little thing about channels is they can masquerade as iterators. We saw that in the prime sieve. And basically, in stackless Python, when you're using channels, you often use channels to build up more complex constructs. For instance, you may want something like a synchronizer, a semaphore, and you tend to build these things from channels if you find a need for having them. Here's another simple example using a channel. What I do in this is I can force an ordering. I can always make the first, there's two tasklets, first and second. Based on the channel, I can actually force, even though first doesn't run first, I can force it to run first, simply by using the way I use the channels. Basically, even if second runs first, it has to wait until first sends a message. So you get an output. You can do more complex logical orderings this way. However, as simple as channels are, the synchronous nature of the channels and the cooperative scheduling allow deadlock conditions to occur. This is something you see newbie programmers, if you go through the stackless mailing list, you'll see quite a few people are saying, well, I have all these tasklets and they're all sharing channels and they're in this nice network and nothing happens. Problem is it's often silent. What stackless does, it's a quirk of the scheduler. When there are no more tasklets that are runnable, it makes the assumption that there is deadlock and it simply terminates. And sometimes it doesn't even say this, it just terminates and it's up to you to know. And here's an example that will create deadlock. All the four fundamental conditions of deadlock occur. No preemption, no mutual exclusion, you have hold-in-weight and you have circular weight. They all occur in this little example. And although I'm not going to really dive into this, but often a little trick you can do is, this is normally used for pickling. You can use the reduce function. So what I've done is, pretend the rest of the program is up there, I do a stackless run, it just drops out. And I'm wondering, hey, what the hey? So I'm basically printing out the state of the two channels. And what I've done is I've just made the names nice. Really it's like tasklet reference at blah, blah, blah. And what you see is you can see A waiting on that little list. You remember that picture I have of the list connected to the channel? Well, that's it. So that's a trick for debugging. Okay, now we're going to get into a very common idiom that you find in stackless Python programs, is often you have sort of like two objects in ascend-receive, sort of like ascend-receive protocol. And what you do is you hide the request channel with a proxy or some interface. And what you do is the client tasklet will create its own channel and send it as a part of the request. The server will munch on it and use and write back a result. This is a very, very common idiom. And we're going to revisit it in a much bigger way. And that's the example of it. Sorry for using a global. And if we did this, all you basically see is the request handler just answering that, yeah, I got the reply. Very simple, but this technique is used a lot. And we're going to move to the last part before we have enough stackless Python to do complicated things. Channel preferences. Stackless Python has a very, very simple scheduler. However, there is a limited form of schedule prioritization. It's called a channel preference. A channel preference is a property of a channel. If you set it to minus one, it's receiver preference. If you set it to one, it's sender preference. If you set it to zero, it's neutral. The main distinction between the preferences is this. If you are a preferred object, what the scheduler will try to do is it will try to put you at the head of the runnable list right away because the idea is there's data waiting for you. If you're a receiver, there's data waiting for you. If there's a sender, there's someone out there who can get information from you. And it's got some interesting consequences. One of them is it allows channels to simulate queues. Here's a very simple example. Again, yeah, I shove the global there. I didn't feel like writing get off, but I'll do that when I write a better example. So basically we have a whole heap of consumers and one producer. And when the channel preference, then I'll show you what the differences are when you have receiver preference. In that case, the receiver preference is on the producer. So basically what it's doing is it's going through all the... it's grabbing data from all the consumers until there are no more and then it writes them out. So basically this is the producer just operating on all its data and then all the other guys get to run when he's sending information back. But with sender preference, it changes. It interleaves. Maybe the main... outside of being a source of confusion for people from the few tests of run, the only real significance I find of this is if the numbers are sufficient... if the numbers are sufficiently large, it starts to have an impact on the execution and the response times. You can sort of see this. On the first receiver, look how long it takes me until I get a message back. However here, I start to get responses back almost immediately. Again, this is something you see in an operating systems book. It's covered quite early. And finally again, when the numbers are large, again you can see that I'm only making one context switch here and I've got about ten context switches there. But context switches are very, very fast. So until you get very big numbers, it really doesn't matter in most cases. So that's all for part two. Let's move into limitations. So far, you know, I've been showing you the upside of Stackless Python, but you know, so you must be wondering, well, why don't we all use it? Yeah, why don't we all use it? Well, it's got some problems. One of the limitations of Stackless Python is it uses a global interpreter lock. But then again, same as C-Python, the second problem is Stackless Python tasks with certain user space. Again, basically the basic answer when people say, yeah, but you know, Stackless Python for all these, you know, threads it can create, you know, it's using a GIL and most people say, well, look, you know, Stackless Python in many ways is based on C-Python and PyPy, and when they solve that problem, Stackless Python will follow. It's sort of flipped, but that's the reality. Let's get on to the real problem. User space threads. As I said before, Stackless Python tasklets and the tasklets and the scheduler run in the operating system's user space. When a tasklet makes a system call that blocks the operating system, what happens is if these tasklets are running, the tasklets are running in OS threads and are running in an application. So when a tasklet makes a blocking system call, it blocks its associated OS thread. And sometimes that's just tantamount to blocking the entire program. So it doesn't matter if you have 10,000 tasklets running. If one blocks, everything blocks. Usually, the reason people care about this is they're using Stackless Python for some form of network programming. That's what most people do. And the solution is you use tasklets in conjunction with an asynchronous IO package, and you've probably seen Async Core, LibEvent, or Twisted. So now we're going to get into the integration techniques. One thing before I get into it is, again, there's another interesting design pattern called the half Async. Again, it comes out of Doug Schmidt. He's the guy that really works on network concurrency patterns. And a major, and Schmidt says that this originally came out of BSD, BSD Unix. But there's a very interesting consequence of using this design pattern is you get a good compromise between the programming ease of a synchronous model combined with the performance of an asynchronous model. I don't want to get too much into that. But again, if you read the Twisted mailing list, you read the Stackless mailing list, you read literature on concurrency. You see this classic debate against event-based models versus thread-based models. There's papers like, oh, who's the guy who invented TCL, TCL John Uderhout? Osterhout. He's got, why threads are a bad idea? And then there's guys who write papers. No, excuse me, why threads? He writes, why are threads a bad idea? And then there's other guys who've come out with a paper. Why events are bad ideas? And it just goes on and on. Anyway, we're going to shift gears once more and talk about the solution. How many of you have heard of reactors? OK. Most asynchronous IO packages are based on something called the reactor pattern. Most operating systems have support for this pattern, i.e., the select function, E-Pole, or, I guess, wait for multiple events in IO ports in Windows NT, or I should say the Windows operating system. Reactors are typically associated with event-driven programming. Reactors provide a limited form of non-preemptive multi-tasking without threads. Again, this is a very important point that Schmidt makes the original paper, that reactors really are schedulers themselves and they promote a non-preemptive form of multi-tasking. So the general strategy is the client, that is, the program, in our case, it's a tasklet but it could be a function. It registers a callback function associated with an event. Maybe it's a receive with the reactor. And then the function, that function ends. When the event occurs, the reactor calls the callback, passing it a result. Since I use twisted, that's a very, very simple twisted example. This is us basically saying what the function we want. We add the callbacks. And when we do reactor run, this whole thing occurs. And these are the callbacks. It's, unfortunately, I didn't, I, okay, when I gave the talk at PyCon 2008 called Adventures in Stackless Python Twisted Integration, I know that's a bit of a mouthful. I had diagrams that showed how twisted control flow works. Just showing you step by step. It's interesting what, basically in Twisted you have two phases. The first phase is the code makes the call. Under the hood, it's writing to a list of, there's about three, there's three lists. There's a reader set, a writer set. And I think there's an exception set. But you have these sets. And then in the second phase, the event, the twisted event loop reads these sets and carries out the operations. So there's like these two strokes in Twisted. So anyway. So the basic integration technique is you associate a callback with a channel. Here's a technique used by one of the twisted, one of the twisted guys who also happens to play with Stackless Python. Do you notice that it looks a heck of a lot like the idiom I showed earlier? It's almost the same. Except he uses an inner, he, in his case, uses an inner class. Basically, think of it this way. The, the, pretend there's a tasklet here. It, it wants to do a get page. So get page returns a deferred. It's creating a channel. It sets up the callbacks, registers them. And then it does a channel receive. Eventually what happens is the reactor fires off the, oh, okay, there's a little miss. Oh, I made a little mistake here. I, bad, bad, bad. I forgot to do this. You do a channel send. It's not a return. That's silly, silly, silly. And you send back that tuple. So let me, let me fix this. And what happens is this tasklet will wake up and it will return the result. Okay, that's a mistake. Pardon me? Oh yes, that, yeah, I should put some data there. I'm, yes, like a URL. Yeah. Yes, I'll fix that. Okay, the second, the second part of the technique is you run the reactor in its own tasklet. Now, as far as twisted, as far as stackless is concerned, the reactor is just another tasklet. It looks like just another, in that example where I had the single producer and many consumers, it's just another tasklet which gets requests and it pumps out results on the channel. And from the reactor's point of view, it doesn't even know about, about tasklets. It's just dealing with callbacks. The last part is you have to make the reactor tasklet yield. Again, intrinsically stack, okay, intrinsically packages like twisted don't know about stackless. They don't. You have the same problem if you're dealing with something like let's say WX-Python or any GUI application. They basically, they're a part of what they do is they, they are schedulers and they think they own the machine. And you don't want to alter, you don't want to alter twisted to put stackless-isms in it. So anyway, where I'm going with this is it doesn't know, it intrinsically doesn't know about stackless. So what happens is it's running and if it doesn't yield, tasklets that can run will not get a chance. So you have to have a technique for making them yield. Fortunately, in a case of twisted, you can do something very nice. You can use a task looping call. Normally I would just put the stackless schedule here, but for some reason I'm getting a weird error and I haven't hunted it down yet. But anyway, the general idea is what a looping call does is every so often what will happen is under the hood when the reactor's coming out of e-pole or select. What it will do is it will call, it will call the looping call. Usually this is used to schedule regular events. So all we do is we tell it yield. So other guys get to run. And I don't have an entire example, but this is what a fragment would sort of look like. Now there's one very interesting thing if you code in twisted. You notice that everything now looks synchronous. The call really, you don't see callback code anymore. And yeah, I think I got it right here. Again, the guys in twisted are trying to do something like this with something called have you ever used an inline generator? That's what inline generators are trying to do. So so far we've shown, I've shown you it from the viewpoint of being a client. I'm a client and I'm making calls out. But what happens if I'm a server? It's basically the same thing, but in reverse. And you have one of two choices. You can either create a tasklet. And it's sort of rendezvous. It's sort of like you create a tasklet and it has some mechanism for telling the underlying listener, hey, I'm listening to, let's say, this URL. And when there's a request on that, it wakes it up. Or what you can do is it could, when there's a new request, I simply create a tasklet and handle it. I'll post code for this, because most of the work I'm doing, I was doing with WS Beeple comes out of this. Again, again, I'm sorry that this is, it's twisted oriented. But basically, actually, again, I need to draw, that's what I was sort of talking about. It's sort of, you can think of time going this way. Think of some request, basically, in the beginning, you have a tasklet. It says, I'm interested in a particular event. Let's say, you know, a request comes in. You have some mechanism, you're passing it a channel and you're saying, I'm waiting for this URL. And it goes to sleep. Then some, then later on, a request comes in from the internet. The request handler simply talks to this guy. There's a match and it wakes him up and gives him information like a return channel. The guy does this stuff and says, okay, I've done, I've finished, I've finished processing and this is the response I want to give. And you give it back to the request handler and it gets sent out. And again, that's what that little piece of code basically does. I guess, I guess the reason I care so much about this, and again, it's from, from adventures, is I ran into a really severe case of deadlock. Because what I had is this tasklet, basically, it's acting like a CGI script. However, what I had this guy doing, I had this guy also calling out to the internet. Yes, two other databases. In WS Beephole, this is called architect, what's the term? Architectural composability. It's sort of like, think of it as the unit principle where you have a whole bunch of pipes and filters and you're connecting them to create steadily larger applications. Well, in the world of web services, architectural composability is a huge part of service oriented architectures. You have these guys and they're calling out to other guys and all together they're creating, you're basically creating bigger apps. The last word, so far I've showed you a lot of the machinery. The interesting thing about the machinery I've shown you, and I sort of hinted at it already, you don't only use it for networking. This also applies to windowing. If you were playing with something like Pygame, you'd use similar techniques. For most of these things, like windowing systems, you're usually like an idle and you would hook in a schedule and not a idle function. But anyway, the final word is stackless socket. Basically, for most programmers, you really don't have to do this. It's nice to know what's under the hood, but you can get away with not knowing it. There is a module called stackless socket. It's a plug-in replacement for sockets. It was developed, I believe, by Andrew Dalkey and Richard, too. The idea is you create a proxy class that implements the socket interface. But under the hood, it's using a channel and an asynchronous package. So you just drop it in and it just automagically works for a lot of third-party libraries. Anyhow, the future, the last part. Basically, you can argue that there's two tracks to the future of stackless. One of them is C-based stackless Python. Develops in step with C-Python. Basically, each new release, some wonderful guys typically from CCP Games. They sit down and they do the necessary changes. I believe you have to alter around 5% of the code to get stackless Python to port the changes to stackless Python and away you go. However, there's this other track called PyPy. There's a lot of overlap between PyPy, stackless Python, and Cyco in terms of the implementers. Christian Tismur's probably... Yeah, Christian works on PyPy. Basically, what PyPy is, it's a Python-based framework for developing interpreters. One of its original main ideas was to prototype future Python features in Python. Also, they want to eliminate the need to write C extensions. Okay, what's happened with PyPy is this. They've basically implemented stackless... The stackless Python, they've done two interesting things. They've done two interesting things with stackless Python. First of all, they've busted apart stackless PyPy's model. Let's say it's gotten much more richer. You have coroutines and you have greenlets. Also, what they've done to stackless Python itself, and the main distinction between coroutines, greenlets, and tasklets is tasklets are rather high level. Tasklets sort of presuppose a scheduler. Coroutines don't. So basically, if you wanted coroutines, the programmer is more in charge of when to do the context switches. The other neat thing that's happened is this. The stackless Python module itself is now written in Python. So if you go there and take a look at the stackless.py, you can see all these algorithms are described for how stackless Python works under the hood. You can look at them and they're in Python. And also, if you want to create, if you want to make, let's say, you want to add new mechanisms to stackless Python. A good way to prototype is, I guess you just branch stackless Python, the stackless Python module, and put your changes and try them out. And maybe if they work, then, and you're a brave soul, then maybe you want to put them into the C implementation. And I guess eventually, when Python, when stack, I mean when PyPy gets sufficiently good performing, that won't even be necessary. So anyway, I guess this is the end of my talk. Thank you for your time.