schedlua – refactor compactorPosted: August 31, 2016
The subject of scheduling and async programming has been a long running theme in my blog. From the very first entries related to LJIT2Win32, through the creation of TINN, and most recently (within the past year), the creation of schedlua, I have been exploring this subject. It all kind of started innocently enough. When node.js was born, and libuv was ultimately released, I thought to myself, ‘what prevents anyone from doing this in LuaJIT without the usage of any external libraries whatsovever?’
It’s been a long road. There’s really no reason for this code to continue to evolve. It’s not at the center of some massively distributed system. These are merely bread crumbs left behind, mainly for myself, as I explore and evolve a system that has proven itself to be useful at least as a teaching aid.
In the most recent incarnation of schedlua kernel, I was able to clean up my act with the realization that you can implement all higher level semantics using a very basic ‘signal’ mechanism within the kernel. That was pretty good as it allowed me to easily implement the predicate system (when, whenever, waitForTruth, signalOnPredicate). In addition, it allowed me to reimplement the async io portion with the realization that a task waiting on IO to occur is no different than a task waiting on any other kind of signal, so I could simply build the async io atop the signaling.
schedlua has largely been a Linux based project, until now. The crux of the difference between Linux and Windows comes down to two things in schedlua. The first thing is timing operations. Basically, how do you get a microsecond accurate clock on the system. On Linux, I use the ‘clock_gettime()’ system call. On Windows, I use ‘QueryPerformanceCounter, QueryPerformanceFrequency’. In order to isolate these, I put them into their own platform specific timeticker.lua file, and they both just have to surface a ‘seconds()’ function. The differences are abstracted away, and the common interface is that of a stopwatch class.
That was good for time, but what about alarms?
The functions in schedlua related to alarms, are: delay, periodic, runnintTime, and sleep. Together, these allow you to run things based on time, as well as delay the current task as long as you like. My first implementation of these routines, going all the way back to the TINN implementation, were to run a separate ‘watchdog’ task, which in turn maintained its list of tasks that were waiting, and scheduled them. Recently, I thought, “why can’t I just use the ‘whenever’ semantics to implement this?”.
Now, the implementation of the alarm routines comes down to this:
local function taskReadyToRun() local currentTime = SWatch:seconds(); -- traverse through the fibers that are waiting -- on time local nAwaiting = #SignalsWaitingForTime; for i=1,nAwaiting do local task = SignalsWaitingForTime; if not task then return false; end if task.DueTime <= currentTime then return task else return false end end return false; end local function runTask(task) signalOne(task.SignalName); table.remove(SignalsWaitingForTime, 1); end Alarm = whenever(taskReadyToRun, runTask)
The Alarm module still keeps a list of tasks that are waiting for their time to execute, but instead of using a separate watchdog task to keep track of things, I simply use the schedlua built-in ‘whenever’ function. This basically says, “whenever the function ‘taskReadyToRun()’ returns a non-false value, call the function ‘runTask()’ passing the parameter from taskReadyToRun()”. Convenient, end of story, simple logic using words that almost feel like an English sentence to me.
I like this kind of construct for a few reasons. First of all, it reuses code. I don’t have to code up that specialized watchdog task time and time again. Second, it wraps up the async semantics of the thing. I don’t really have to worry about explicitly calling spawn, or anything else related to multi-tasking. It’s just all wrapped up in that one word ‘whenever’. It’s relatively easy for me to explain this code, without mentioning semaphores, threads, conditions, or whatever. I can tell a child “whenever this is true, do that other thing”, and they will understand it.
So, that’s it. First I used signals as the basis to implement higher order functions, such as the predicate based flow control. Now I’m using the predicate based flow control to implement yet other functions such as alarms. Next, I’ll take that final step and do the same to the async IO, and I’ll be back to where I was a few months back, but with a much smaller codebase, and cross platform to boot.