Mixed model
Handlers and closures
Coroutines
The Idle handler
Coroutine notes
Window redrawing
Mixed model
gpeddler 2 is rather incoherent as far as the programming model
is concerned: for example MainFrame, Route, Solver and Options are designed in object-oriented fashion
(ad described in Simple OO in Lua), Place is treated as a "half-object" (functions
are kept separate from data) and HelpFrame
is just a hastily-written function with associated static data.
This is by choice: I am not a purist; I try to get the most advantages
from different approaches, even if it means shamelessly mixing different
programming models in the same program. Some choices are of course
influenced by the small size of the application.
In fact, I could have avoided using proper objects in MainFrame (there is only one main frame, after
all), but I wanted to try out my OO approach in a realistic situation.
Maybe I was also influenced by my experience with C++ frameworks...
anyway, this allows multiple copies of the main window to be
instantiated, even if that would not be significant for this application.
Speaking of objects, instead of designing the ExhaustiveSolver
and EvolutionarySolver classes (as
subclasses of Solver), I could have
created and modified two instances of Solver.
That would have been a readability crime, however, and I would also
have lost my example of inheritance.
Handlers and closures
Closures are a powerful feature of Lua, but they are not easy to intuitively
grasp at first, especially if your experience is from static languages
(as mine mostly is). While writing this program, I encountered a
situation where they provide a simple and elegant solution to a problem
most programmers will recognize: callbacks in an object-oriented
environment. It really helped me understand closures, at least at a
first level.
This is a line from Solver.Create(), the Solver class constructor in the file solver.lua, formatted into three lines for
convenience:
self.window:ConnectEvent(wx.wxEVT_PAINT,
function(ev) self:Repaint() end
)
It connects an unnamed event function to the given window; in
other terms, every time the window sends a wx.wxEVT_PAINT
event, the function built in the second line gets called. This function,
in turn, calls the Repaint() function that
redesign the window's contents. As the unnamed event handler function is
called back from wxWindow when required, that's a "callback".
Well, where's the problem? Let's look into this deceptively
simple piece of code to see what happens behind the scenes. Consider the
function:
self:Repaint()
It is translated by the Lua compiler to:
self.Repaint(self)
Since the Repaint field of all Solver objects refer to the same function Solver.Repaint(), the above line is equivalent
to:
Solver.Repaint(self)
This means that the same Repaint()
function is called for different objects. How does the program know which
object has to be repainted? By looking at the self
variable, of course. If we have two different Solver
objects:
solver1 -- a Solver
solver2 -- another Solver
we actually need their respective wx.wxEVT_PAINT
handlers to make two different function calls:
solver1:Repaint() -- repaint the first Solver's window
solver2:Repaint() -- repaint the second Solver's window
which is a compact and more efficient way of saying:
solver1.Repaint(solver1)
solver2.Repaint(solver2)
The problem here is not the calling of different functions, but the
calling of the same function with a different argument: either solver1 or solver2
in our example. The argument, however, is known now (when the
unnamed handler function is created) but will need to be used later
(when the unnamed handler function will be called by wxWindows): it has
to be saved somewhere.
Here we get to closures (at last). When this expression is
executed:
function(ev) self:Repaint() end
The current value of the variable self
is used in creating the new unnamed function, and it is remembered
inside the function. In other words, this piece of code:
self = solver1
function(ev) self:Repaint() end
creates a function that permanently refers to the value that self had at the time of function creation,
as if we had written:
function(ev) solver1:Repaint() end
or, more explicitly:
function(ev) solver1.Repaint(solver1) end
which (by the way) in the case of all objects pointing to the same Repaint() function, means:
function(ev) Solver.Repaint(solver1) end
Since in the constructor Solver.Create()
the variable self refers to the current
object just created in the constructor itself, our trivial-looking
code:
self.window:ConnectEvent(wx.wxEVT_PAINT,
function(ev) self:Repaint() end
)
will create a different unnamed handler function every time, and then
pass it to ConnectEvent(). Each of these
functions, when called back from wxWindows, will call Solver.Repaint() with a different argument:
the current object (i.e. the value of variable self)
at the time the function was created, which is exactly what we want.
In short, closures can be used to build a function at runtime, setting
the values that some variables (in our case, self) will have inside the function itself.
Coroutines
gpeddler 2 runs on a single thread, yet it manages to run two
solving algorithms in parallel (showing their progress in two different
windows) and to respond promptly to user interface commands, all
apparently at the same time. It does this by using two different
techniques: coroutines and idle events.
Modern operating systems offer preemptive multitasking within a
single application: there can be many virtually independent flows of
control, usually caused "threads".
Lua, being strictly ANSI-C based, cannot do this in a truly portable
way (even if there are provisions for using external thread
functionalities). It offers instead, starting from version 5.0, cooperative
multitasking built into the language itself under the name of
"coroutines".
While threads are completely transparent, i.e. they know
nothing about the existance of other threads, coroutines are not:
they must cooperate by executing a short piece of their code and then
giving other coroutines an opportunity to run.
A function launched as a coroutine must periodically call coroutine.yield(); this has the effect of suspending
its execution, remembering its whole status, and returning to the
calling function. A subsequent call to coroutine.resume() continues
the execution as if nothing had happened in between.
In gpeddler 2, a solver coroutine is started by Solver:StartSolving() in the file solver.lua:
self.solverCo = coroutine.create(self.Solve)
local r, err = coroutine.resume(self.solverCo, self)
The first line creates the coroutine, the second line starts executing
its code. A typical function executing as a coroutine has a structure
similar to this one:
-- launched as couroutine
--
function doit()
while not finished do
-- perform a bit of work
coroutine.resume()
end
end
This is a bit more involved in the ExhaustiveSolver:Solve()
and EvolutionarySolver:Solve() functions in
gpeddler 2, but nethertheless it boils down to just calling coroutine.yield() more or less periodically.
A good compromise should be made here: if the coroutine function
yields too often, much time could be lost in task switching (rather than
working); on the other hand, if too much time passes between coroutine.yield() calls, the other coroutines
will be starved of CPU resources (and the user interface could become
unresponsive).
When a coroutine calls coroutine.yield()
to stop working and allow the rest of the system to have some CPU time,
somebody else has to later call coroutine.resume()
to make the coroutine processing continue, or it would wait forever.
For example, there could be a scheduler, that could be as
simple as:
while not finished do
coroutine.resume(coroutine1)
coroutine.resume(coroutine1)
-- etc.
end
gpeddler 2 uses this approach, but adds another multitasking level to
handle interface requirements, as described below.
The Idle handler
In an event-driven environment like wxWindows, it is not a good idea to
stop responding to events because the program is currently doing
something else: events should be served as soon as possible.
Windows contents should be promptly redesigned at the system's request,
and user feedback should never give an impression of sluggishness.
Therefore, event handling takes precedence; the program must be always ready
to serve events on short notice. In other words, all event handler
functions should perform their duty and return fairly quickly.
In gpeddler 2 the rest of the work, which is the actual work from the
user's point of view, is done while the system (meaning the wxWindows
runtime) is idle, i.e. there are no events to serve.
This is the duty of the MainFrame:OnIdle()
function, installed by the initialization function MainFrame:Init()
that is called automatically when there are no pending events and mainly
acts as coroutine scheduler:
function MainFrame:OnIdle(ev)
self.solver1:ContinueSolving() -- can be called even if not
active
self.solver2:ContinueSolving()
-- enable/disable interface elements
self:UpdateInterface()
ev:RequestMore() -- call me again if still idle
end
It just does a small piece of work for each of the solvers by calling coroutine.resume() for each of them, updates the
interface to the current state of the solvers (e.g. by enabling the
"Stop" button if a solver is running) and then tells the system to call
again this idle function as soon as there will be no pending events.
So, in absence of events, the idle function actually runs as if in a
loop; on the other hand, if there are events they are immediately
handled.
The ContinueSolving() function just call coroutine.resume() and prints out information
about any error that could happen inside the coroutine itself.
Coroutine notes
coroutine.yield() and coroutine.resume()
can also be used to pass extra information back and forth.
gpeddler 2 uses this opportunity to send a 'stop request' to a solver
coroutine. The information is sent as second argument of coroutine.resume() by Solver:StopSolving():
local r, err = coroutine.resume(self.solverCo, true)
and received by ExhaustiveSolver:Solve()
or EvolutionarySolver:Solve(), both of
which run as coroutines:
stopRequest = coroutine.yield() -- true = termination request
(this is actually a bit more complex in EvolutionarySolver:Solve(),
but it is basically the same)
The other coroutine communication channel, i.e. passing back
information using a coroutine.yield()
argument, is not used in this program. It could be used, for example, to
inform the coroutine caller about the current status of the coroutine
process.
Some of the most significant drawbacks of using
coroutines (as opposed to threads) are:
- Multitasking is not fully transparent to the programmer.
- There is the added red tape of having to call coroutine.yield() at short intervals.
- A scheduler (or equivalent mechanism) should be provided by the
programmer.
- Scheduling in a high-level language (such as Lua) is slightly
less efficient.
- Equal time-processing among coroutines cannot be guaranteed,
because it depends on the time spent between coroutine.yield()
calls.
On the other hand, there are many advantages:
- Lua programs using coroutines are fully portable among different
operating systems.
- There are no problems in concurrent access to shared data: a
coroutine cannot be interrupted between calls.
- The hand-made scheduler gives better control over coroutine
priorities and time-sharing.
- Designing and debugging coroutines is a lot easier.
Window redrawing
A program in which three separate flows of control (the user interface
and the two solvers) could access the screen at the same time
usually tends to have problems. Fortunately, both wxWindows and Lua
offer tools to prevent such problems happening.
wxWindows gives the programmer two different ways to access a
window for drawing, available through wxLua as wx.wxPaintDC
and wx.wxClientDC.
The first one is used when redrawing is required by the system, i.e.
inside an OnPaint event handler, while wx.wxClientDC can be freely used from the
program to draw as it whishes. This prevents conflicts between
predictable drawing performed by the program (i.e. the solvers) and
asyncronous drawing executed in response to a window redraw request.
gpeddler 2, however, ignores this opportunity and uses an even cleaner
approach: the solvers never draw anything on their window. They
just invalidate the window contents to tell the system that it needs to
be redrawn. For example, in Solver:EvaluateRoute()
in the file solver.lua:
-- request window redrawing with no background drawing
self.window:Refresh(wx.FALSE)
The system, in turn, will call the window repaint handler Solver:Repaint() installed by the constructor Solver.Create():
self.window:ConnectEvent(wx.wxEVT_PAINT, function(ev)
self:Repaint() end)
For this approach to work satisfactorily, there should be no
perceivable delay between the invalidating call to Window:Refresh() (inside a solver coroutine) and
the system-initiated call to the repaint handler. This is achieved by
making sure that the solving coroutines do not spend too much time
before yielding CPU control by calling coroutine.yield().
Text labels, on the other hand, in this program are just redrawn
as needed, leaving to wxWindows the responsibility to avoid possible
redrawing conflicts (presumably by the internal use of wxClientDC).
A possible speed and redrawing improvement could have been the use of a buffer
bitmap to have flicker-free text labels, but that would have
probably been excessive for such a simple program.
Back to start of page