The scheduler — How tasklets are run¶
The two main ways in which Stackless schedules its tasklets arepre-emptive scheduling and cooperative scheduling. However, thereare many ways these two approaches can be used to suit the needsof a given application.
Cooperative scheduling¶
The simplest way to run the scheduler is cooperatively. Programmers stillneed to know when blocking will occur in their code and deal with itappropriately. But unlike pre-emptive scheduling, they can know exactlywhere blocking will occur and this in turn allows them a good ideaabout how their code will behave in cooperation with whatever else maybe running in tasklets.
Example - running a simple function within a tasklet:
>>>deffunction(n):...foriinrange(n):...print(i+1)...stackless.schedule()...>>>stackless.tasklet(function)(3)>>>stackless.run()123
In the example above, a functionfunc is defined, then a tasklet created for itto be called within. The scheduler is then run, which schedules the taskletfour times. The fourth time the tasklet is scheduled, the function exits andbecause of this the tasklet also.
The code to run in a tasklet:
deffunction(n):foriinrange(n):print(i+1)stackless.schedule()
The first step is having code to run in a tasklet. Herefunc simply loopsn times, in each iteration printing whichiteration it is, then giving the other scheduled tasklets a chance to runby callingstackless.schedule().
Creating the tasklet:
stackless.tasklet(function)(3)
Withfunc to run, a tasklet is bound to it and argumentsprovided which in this case are a value of3 forn. When the tasklet isfirst scheduled andfunc first called within it, the arguments arepassed into the call. The act of creating a tasklet in this way automaticallyinserts it into the scheduler, so there is no need to hold a reference to it.
Running the scheduler:
stackless.run()
Next the scheduler is run. It will exit when there are no tasklets remainingwithin it. Note that this tasklet will be scheduled four times. The firstwill be the initial run of the tasklet, which will start a call intofuncwithin it providing whatever arguments were given (in this case forn).The second and third are also the second and third prints, and finally a fourthtime whenstackless.schedule() returns and the function exits andtherefore the tasklet also.
If the developer knows that as long as the application is to run, there willbe tasklets in the scheduler to be run as shown here, the applicationcan be driven by a call tostackless.run(). However, it is not alwaysthat simple and there are many reasons why the scheduler may be empty, andrepeated calls tostackless.run() need to be made after creation ofnew tasklets or reinsertion of old ones which were blocked outside of it.
Detecting uncooperative tasklets¶
In practice, it is very rare that a tasklet will run without yielding to allowothers to run. More often than not, tasklets are blocked waiting for events tooccur often enough that they do not need to explicitly yield. But occasionallyunforeseen situations can occur where code paths lead to yields not being hit,or perhaps bad code enters an infinite loop.
With this in mind, it is often more useful to take advantage of the pre-emptivescheduling functionality, to detect long running tasklets. The way this worksis to pass in a sufficiently high timeout that the only tasklets which hit itare those which are not yielding.
Idiom - detecting uncooperative tasklets:
while1:t=stackless.run(1000000)iftisnotNone:t.insert()print("*** Uncooperative tasklet",t,"detected ***")traceback.print_stack(t.frame)
Scheduling cooperatively, but pre-empting uncooperative tasklets:
t=stackless.run(1000000)
As most tasklets will yield before having executed1000000 instructions,the only tasklets which will be interrupted and returned will be those thatare not yielding and therefore being uncooperative.
Ensuring the interrupted tasklet resumes:
iftisnotNone:t.insert()
Interrupted tasklets are no longer in the scheduler. We do not know what thistasklet was doing, and to leave it uncompleted may depending on our applicationbe unacceptable. The call totasklet.insert() puts the it at theend of the list of runnable tasklets in the scheduler, forcibly ensuring theothers have a chance to run before it gets another.
It might also be reasonable to assume that any tasklet that gets interruptedin this manner is behaving wrong, and that to kill it having recorded asmuch information about it as possible (like its call stack) before doing sois better.
Killing the interrupted tasklet:
iftisnotNone:print("*** Uncooperative tasklet",t,"detected ***")traceback.print_stack(t.frame)t.kill()
Note
Tasklets that do long running calls outside ofPython® code are not somethingthis mechanism has any insight into. These calls might be doingsynchronous IO, complexmath module operations that execute in theunderlying C library or a range of other things.
Pumping the scheduler¶
The most obvious way to use Stackless is to put all your logic into taskletsand to run the scheduler, where there is an expectation that when thescheduler exits so does your application. However, by taking this approachyour application is built within the Stackless framework and has to bestructured around being run within the scheduler.
This may be unacceptable if you want more control over how your applicationruns or is structured. It may seem to rule out the use of cooperativescheduling and push you towards pre-emptive scheduling, so that yourapplication or framework can drive Stackless instead.
However, there is a way to retain the benefits of cooperative schedulingand still have your application or framework in control. This is calledpumping the scheduler.
Idiom - pumping the scheduler:
defApplicationMainLoop():while1:ProcessMessages()ApplicationLoopStuff()Etc()stackless.run()RescheduleBlockedTasklets()
Pumping the scheduler works by having code that explicitly yields,yield onto a channel, instead of callingstackless.schedule()and instead yielding back into the scheduler. By doing so, thescheduler is empty after each tasklet that was scheduled has run.This means that a scheduler run is effectively an act of runningeach scheduled tasklet once, and it can be pumped within anapplication or frameworks main loop.
Yielding outside of the scheduler:
defCustomYield():customYieldChannel.receive()
Defining a custom function that code can call to yield their taskletsoutside of the scheduler, is as simple as having a channel for themto wait on. There will never be any tasklets sending on the channel,so those that call will always block onto it.
Rescheduling blocked tasklets:
defRescheduleBlockedTasklets():whilecustomYieldChannel.balance<0:customYieldChannel.send(None)
When we want to reinsert all the blocked tasklets back into thescheduler, we simply do sends on the channel as long as there arereceivers. However, there is one situation we want to avoid. We donot want to run each receiving tasklet as we do a send to it. Inorder to avoid this, we need to make sure that our channel simplyinserts the receiving tasklet into the scheduler to be run in duecourse instead.
Configuring the channel to be yielded onto:
customYieldChannel=stackless.channel()customYieldChannel.preference=1
This is a simple change to thechannel.preference attributeof the channel when it is created.
Note
If you pump the scheduler, your tasklets cannot callstackless.schedule(). To do so, without knowledge of whatyou are doing, will result in a tasklet that continuouslygets scheduled. And the call tostackless.run() will notexit until the tasklet yields in another manner out of thescheduler, errors or exits.
Pre-emptive scheduling¶
If you want a lot of the work of using operating system threads without a lotof the benefits, then pre-emptive scheduling is a good choice. Making thescheduler work in a pre-emptive manner, is a simple matter of giving it atimeout value.
Example - running a simple tasklet within the scheduler:
>>>deffunction():...i=0...whileTrue:...print(i+1)...i+=1...>>>stackless.tasklet(function)()>>>stackless.run(100)1234567891011<stackless.tasklet object at 0x01BCD0B0>
In this case, the scheduler runs until a maximum of100 instructions havebeen executed in thePython® virtual machine. At which point, whatever taskletis currently running is returned whenstackless.run() exits. Thestandard way to employ this is to pump the scheduler, reinserting theinterrupted tasklet.
Idiom - pre-emptive scheduling:
whileTrue:ProcessMessages()ApplicationLoopStuff()Etc()t=stackless.run(100)iftisNone:breakt.insert()
Run the scheduler for100 instructions:
t=stackless.run(100)
There are two things to note here, ift isNone then there are notasklets in the scheduler to run. Ift is notNone, then it is aninterrupted tasklet that needs to be reinserted into the scheduler.
Detect an empty scheduler:
iftisNone:break
It may be that an empty scheduler indicates that all the work is done, or itmay not. How this work is actually handled depends on the implementationdetails of your solution.
Reinsert the interrupted tasklet:
t.insert()
Note
You are not running the scheduler for100 instructions, you arerunning it until any subsequently scheduled tasklet runs for at least thatmany instructions. If all your tasklets always explicitly yield beforethis many instructions have been executed, then thestackless.run()call will not exit until for some reason one does not.
Running the scheduler forn instructions¶
Running the scheduler until a scheduled tasklet runs forn consecutiveinstructions is one way pre-emptive scheduling might work. However, if youwant to structure your application or framework in such a way that it drivesStackless rather than the other way round, then you need the scheduler toexit instead. The scheduler can be directed to work in this way, bygiving it atotaltimeout flagvalue.
Idiom - pre-emptive scheduler pumping:
whileTrue:ProcessMessages()ApplicationLoopStuff()Etc()t=stackless.run(100,totaltimeout=True)iftisNone:breakt.insert()
Exceptions¶
Exceptions that occur within tasklets and are uncaught are raised out of thestackless.run() call, to be handled by its caller.
Example - an exception raised out of the scheduler:
>>>deffunc_loop():...while1:...stackless.schedule()...>>>deffunc_exception():...raiseException("catch this")...>>>stackless.tasklet(func_loop)()<stackless.tasklet object at 0x01C58EB0>>>>stackless.tasklet(func_exception)()<stackless.tasklet object at 0x01C58F70>>>>stackless.run()Traceback (most recent call last): File"<stdin>", line1, in<module> File"<stdin>", line2, infunc_exceptionException:catch this
This may not be the desired behaviour, and a more acceptable one might be thatthe exception is caught and dealt with in the tasklet it occurred in beforethat tasklet exits.
Catching tasklet exceptions¶
We want to change the new behaviour to be:
- The tasklet with the uncaught exception exits normally.
- The uncaught exception is examined and handled before the tasklet exits.
- The scheduler continues running.
There are two ways to accomplish these things. You can either monkey-patchthe tasklet creation process, or you can use a custom function for all yourtasklet creation.
Example - a custom tasklet creation function:
defnew_tasklet(f,*args,**kwargs):defsafe_tasklet():try:f(*args,**kwargs)exceptException:traceback.print_exc()returnstackless.tasklet(safe_tasklet)()new_tasklet(some_function,1,2,3,key="value")
Example - monkey-patching the tasklet creation process:
def__call__(self,*args,**kwargs):f=self.tempvaldefnew_f(old_f,args,kwargs):try:old_f(*args,**kwargs)exceptException:traceback.print_exc()self.tempval=new_fstackless.tasklet.setup(self,f,args,kwargs)stackless.tasklet.__call__=__call__stackless.tasklet(some_function)(1,2,3,key=value)
Printing the call stack in the case of an exception is good enough for theseexamples, but in practice the call stack might instead be recorded in adatabase.
Note
We catchException explicitly, rather than catching any exceptionwhich might occur. The reason for this is to avoid catching exceptions weshould not be catching likeSystemExit orTaskletExit, which derive from the lower levelBaseException.
