Figure 7.1 shows an example of
an interaction between the main components of the system in runtime.
We adopted the notation used in [GHJV95] to which we
superposed the activity of the threads. The time flows from top to
bottom. The vertical rectangles show when an object is active. A
method call is represented by an arrow from the calling to the called
object. The label above the arrow indicates the method name. We have
tried to visualize the switching between the three threads: when a
vertical rectangle is shaded gray, the thread in which the object is
called is suspended. On the right hand side is indicated which of the
threads is active.
|
|
Figure 7.1: The interaction diagram shows the interaction
of the main components of the system in time (see text for a more
detailed explication.) (ET = Event Thread, ST = Synthesis Thread.)
|
In figure 7.1 the event thread
calls a program to handle an add-event. The application of the event's
program provokes the creation of a synthesis process. To store the new
object storage space is requested from the heap. After the creation,
the synthesizer is requested to add the new synthesis process to the
list of active processes. From this point on, the synthesis process is
said to be active. The life of a synthesis process knows four life
stages. When it is just created its state is called
embryo. When it is added to the synthesizer it is called once
with the message birth. The birth method allows the
process to do specific work when it is called for the first time (a
fade-in, for example). The process is then said to be alive
and will subsequently receive the message live. A
kill event (the third event in the figure) will cause a
currently active process to be removed from the synthesizer. The
process is not immediately eliminated; its state is set to
dying, and it gets another chance to finish its song. It is
called once more with the message die in which it can do a
fade-out, for example. In the last stage, called heaven, the
process is removed from the array in the synthesis thread.
We can see in the figure that the event thread is constantly
interrupted by the synthesis thread. Since the synthesis is more
important than the event handling, it has priority. The synthesis is a
hard real-time task and should never be delayed since this would
introduce audible clicks in the sound output. The synthesis thread has
therefore the highest priority. The processing of incoming events is a
soft real-time task. The events should be handled as fast as we can.
If an event specifies the start of a new sound, for example, the delay
should be less than 30 msec. However, it's better to handle the event
a little to late (say 50 msec) than not at all. This task has a high
priority but lower than the synthesis task. The interaction with the
user thru the Scheme shell or any other interface is not, strictly
speaking, a real-time task. Granted, to have a good interaction,
responses to user input should be fast. Nevertheless, this task has
the lowest priority of the three.
|
|
Figure 7.2: Priority inversion (IT = Interpreter Thread,
ET = Event Thread, ST = Synthesis Thread.) |
In most cases the synthesis processes will depend on other objects.
For example, an oscillator will depend on a controller object
indicating the frequency of the oscillator. The oscillator retrieves
the value of the controller at regular time intervals. If the
controller object is defined in the Scheme environment, incoming
events and the subsequent application of programs can modify the state
of the controller. This makes the controller a critical zone (see section 1.3) since it is called by
several concurrent tasks. Access to its shared data must be
synchronized. This is detailed next. Consider the flow of events
shown in figure 7.2. The Scheme
interpreter sends an object the message to change its value. The
method set is a critical region and the object protects its
shared data with a monitor. At that point the synthesis thread wakes
up, suspends the Scheme thread (lowest priority), and calls the
synthesis process. The synthesis process sends the object the message
get to obtain its value. Since the interpreter thread still
possesses the monitor (the set method), the synthesis thread
will block when it tries to enter the object's monitor. As a result
the interpreter thread is resumed again. Before it has had the time to
leaves the critical region an event arrives. Now, the event thread
wakes up and suspends the interpreter thread (still lower
priority). At this point the synthesizer thread must wait for the
event thread to finish handling the event. This event handling may
take very long and the synthesis thread may be delayed for an
unpredictable amount of time. This is the problem of priority
inversion we mentioned in section
1.3. It is a well known problem that is generally solved with the
technique of priority inheritance. In the situation above, the
interpreter thread would inherit the priority of the
synthesis thread, in which case it cannot be suspended by the event
thread. Despite this fact, the synthesis thread still has to wait for
the interpreter thread to leave the critical zone. With priority
inheritance, the maximum delay of the synthesizer is no longer
unpredictable but related to the size of the critical zone.
However, if the computation in the critical zones is reduced to a
minimum, these delays will be reduced to a minimum. In the next
section we continue the study of the interaction in the special case
of the garbage collector.