Posts Tagged ‘inversion-of-control’
Imperative Reactivity
The LINK reactivity primitive was already presented in this post.
LINK makes the use of event-driven paradigm easier, avoiding all the bureaucracy of events definition, posting and handling: they are all done implicitly.
However, in another post, we pointed out three issues in this paradigm: inversion of control, multi-core processors and listener life cycle.
This post deals with the first and more annoying limitation of event-driven programming: inversion of control.
Event-driven programming demands that its callbacks execute very fast as the event handling is serialized: while one event is being handled, others are not – only one callback executes at a time.
Callbacks are, therefore, logically instantaneous and can’t hold state. They commonly receive state (those void* parameters), decode it (to work like a state machine and know where it was left), update it and return.
The sequential execution paradigm is broken here, as control is now guided by the event queue (the environment).
LuaGravity supports reactivity inside its methods in some way like Esterel does.
The AWAIT primitive suspends the execution of a method and waits for another method (or timer) to happen.
For example, AWAIT(keyboard.ENTER.press) suspends the running method until ENTER is pressed.
AWAIT is like a LINK, but is broken when its condition happens and resumes (instead of calling) the suspended method.
A little bit confusing, huh?
Hope the following example helps.
Let’s say we want an animation to change its direction following the sequence RIGHT, DOWN, LEFT and UP when the respective keys are pressed.
Without using the AWAIT primitive, we need to hold state between successive executions of animation.changeDirection:
-- WITHOUT AWAIT
LINK(RIGHT.press, animation.changeDirection)
LINK(DOWN.press, animation.changeDirection)
LINK(LEFT.press, animation.changeDirection)
LINK(UP.press, animation.changeDirection)
function animation.changeDirection (key)
if (obj.state == 'right') and (key == 'DOWN') then
obj.state = 'down'
obj.x = obj.x() -- stay here
obj.y = obj.y() + S(obj.speed) -- move down
elseif (obj.state == 'down') and (key == 'LEFT') then
obj.state = 'left'
obj.x = obj.x() - S(obj.speed) -- move left
obj.y = obj.y() -- stay here
elseif (obj.state == 'left') and (key == 'UP') then
obj.state = 'up'
obj.x = obj.x() -- stay here
obj.y = obj.y() - S(obj.speed) -- move up
elseif (obj.state == 'up') and (key == 'RIGHT') then
obj.state = 'right'
obj.x = obj.x() + S(obj.speed) -- move right
obj.y = obj.y() -- stay here
end
end
With the use of AWAIT, we code the animation sequentially:
-- WITH AWAIT
while true do
AWAIT(RIGHT.press)
obj.x = obj.x() + S(obj.speed) -- move right
obj.y = obj.y() -- stay here
AWAIT(DOWN.press) obj.x = obj.x() -- stay here
obj.y = obj.y() + S(obj.speed) -- move down
AWAIT(LEFT.press) obj.x = obj.x() - S(obj.speed) -- move left
obj.y = obj.y() -- stay here
AWAIT(UP.press)
obj.x = obj.x() -- stay here
obj.y = obj.y() - S(obj.speed) -- move up
end
In the code above, obj.x() takes its current position, and S(obj.speed) animates the objects (position = integral of speed).
Much cleaner with AWAIT, isn’t?
See the result in the video below:
The full example code can be found here.
The problem with Events
Following the discussion from here and here…
Some problems arise from event-driven programming adoption:
- Inversion of control:
- Multi-core processors:
- Listener life cycle:
Commanded by the environment, event-driven programs must quickly answer requests to keep the system up and running.
For instance, if a program wants to perform a time consuming operation (e.g. I/O), it must demand it to be done in the background and register a callback to continue the previous operations. If a logical sequence has three of these time consuming operations, the job must be separated into that many callbacks.
This characteristic leads to difficulty in visually understanding the applications’ control flow and is known as callback soup.
Another bad consequence of this code separation is that the state kept in callbacks are independent of each other as all local variables residing in their stacks are lost on returning. To keep state, their stacks must be manually reconstructed in the heap and passed as extra parameter from callback to callback.
This phenomenon is known as stack ripping.
In this sense, threads offer a more natural way of programming as the programmer may use the conventional sequential flow, keeping all state in thread’s single stack.
Another issue with the event-driven approach is the challenge to take advantage of multi-core architectures.
In a common event-driven dispatcher, handlers must be run in sequence as they all share global state. When parallelized, two handlers would access the shared memory at the same time, leading to data corruption.
This is not the case with threads which are already artificially run in parallel even in single-core, thus having all shared memory protected manually with programmed instructions.
Well written multi-threaded programs take advantage of multi-core for free.
Once a handler is registered for events it remains listening to them until a call to unregister is made.
In some sense, this is similar to manual memory management. The system is not capable of deciding by itself when a listener must be unregistered.
There is no such concept as “listen while some rule is valid”.
The first (and worst) problem is solved with cooperative multi-tasking.
A paper [1] explores this issue and shows this interesting graphic where the sweet spot is viewed as an improvement over both event-driven and multi-threading choices.
The second problem is a new one as multi-core architectures are still debuting.
I do have some ideas on this issue and believe that smarter people also do.
Giving different “colors” to unrelated callbacks is an option already in use [2].
The third problem arose on my own research and I’m searching for works to see if someone had already pointed that out. I’m working on a “listening scope” concept and wondered if something similar exists.
To conclude, I think that event-driven programming had always been seen more as technique than a true paradigm. Support in languages is very limited and always offered as a library layer that creates a small synchronous world for specific tasks.
[1] “Cooperative Task Management without Manual Stack Management”:
www.stanford.edu/class/cs240/readings/usenix2002-fibers.pdf
[2] “Multiprocessor Support for Event-Driven Programs”: people.csail.mit.edu/nickolai/papers/usenix2003-slides.pdf
