Going Up?
by Sam Tregar
|
Pages: 1, 2, 3, 4
The Elevator Class
Each elevator thread contains an object of the Elevator class. The
Elevator::run() routine creates this object as its first activity:
# run an Elevator thread, takes a numeric id as an argument and
# creates a new Elevator object
sub run {
my $self = Elevator->new(@_);
Notice that since $self is not marked shared it is a
thread-local variable. Thus, each elevator has its own private
$self object. The new() method just sets up a hash with some
useful state variables and returns a blessed object:
# create a new Elevator object
sub new {
my $pkg = shift;
my $self = { state => STARTING,
floor => 0,
dest => 0,
@_,
};
return bless($self, $pkg);
}
All elevators start at the ground floor (floor 0) with no destination.
The state attribute is set to STARTING which comes from this set
of constants used to represent the state of the elevator:
# state enumeration
use constant STARTING => 0;
use constant STOPPED => 1;
use constant GOING_UP => 2;
use constant GOING_DOWN => 3;
After setting up the object, the elevator thread enters an infinite
loop looking for button presses that will cause it to travel to a
floor. At the top of the loop $self->next_dest() is called to
determine where to go:
# run until simulation is finished
while (1) {
# get next destination
$self->{dest} = $self->next_dest;
The next_dest() method examines the shared array @BUTTON to
determine if any people are waiting for an elevator. It also looks at
%PANEL to see if there are people inside the elevator heading to a
particular floor. Since next_dest() accesses shared variables it
starts with a call to lock() for each shared variable:
# choose the next destination floor by looking at BUTTONs and PANELs
sub next_dest {
my $self = shift;
my ($id, $state, $floor) = @{$self}{('id', 'state', 'floor')};
lock @BUTTON;
lock %PANEL;
Perl's lock() is an advisory locking mechanism, much like
flock(). When a thread locks a variable it will wait for any other
threads to release their locks before proceeding. The lock obtained
by lock() is lexical - that is, it lasts until the enclosing scope
is exited. There is no unlock() call, so it's important to
carefully scope your calls to lock(). In this case the locks on
@BUTTON and %PANEL last until next_dest() returns.
next_dest()'s logic is simple, and largely uninteresting for the
purpose of learning about thread programming. It does a simple scan
across @BUTTON and %PANEL looking for 1s and takes the first
one it finds.
Once next_dest() returns the elevator has its marching orders. By
comparing the current floor ($self->{floor}) to the destination
the elevator now knows whether it should stop, or travel up or down. First, let's look at what happens when the elevator decides to
stop:
# stopped?
if ($self->{dest} == $self->{floor}) {
# state transition to STOPPED?
if ($self->{state} != STOPPED) {
print "Elevator $id stopped at floor $self->{dest}.\n";
$self->{state} = STOPPED;
}
# wait for passengers
$self->open_door;
sleep $ELEVATOR_WAIT;
The code starts by printing a message and changing the state attribute
if the elevator was previously moving. Then it calls the
open_door() method and sleeps for $ELEVATOR_WAIT seconds.
The open_door() method opens the elevator door. This allows
waiting people to board to elevator.
# open the elevator doors
sub open_door {
my $self = shift;
lock %DOOR;
$DOOR{"$self->{id}.$self->{floor}"} = 1;
cond_broadcast(%DOOR);
}
Like next_dest(), open_door() manipulates a shared variable so
it starts with a call to lock(). It then sets the elevator door
for the elevator on this floor to open by assigning 1 to the
appropriate entry in %DOOR. Then it wakes up all waiting person
threads by calling cond_broadcast() on %DOOR. I'll go into more
detail about cond_broadcast() when I show you the Person class
later on. For now suffice it to say that the people threads wait on
the %DOOR variable and will be woken up by this call.
The other states, for going up and going down, are handled similarly:
} elsif ($self->{dest} > $self->{floor}) {
# state transition to GOING UP?
if ($self->{state} != GOING_UP) {
print "Elevator $id going up to floor $self->{dest}.\n";
$self->{state} = GOING_UP;
$self->close_door;
}
# travel to next floor up
sleep $ELEVATOR_SPEED;
$self->{floor}++;
} else {
# state transition to GOING DOWN?
if ($self->{state} != GOING_DOWN) {
print "Elevator $id going down to floor $self->{dest}.\n";
$self->{state} = GOING_DOWN;
$self->close_door;
}
# travel to next floor down
sleep $ELEVATOR_SPEED;
$self->{floor}--;
}
The elevator looks at the last value for $self->{state} to
determine whether it was already heading up or down. If not, then it prints a
message and calls $self->close_door(). Then it sleeps for
$ELEVATOR_SPEED seconds as it travels between floors and adjusts
its current floor accordingly.
The close_door() method simply does the inverse of open_door(),
but without the call to cond_broadcast() since there's no point
waking people up if they can't get on the elevator:
# close the elevator doors
sub close_door {
my $self = shift;
lock %DOOR;
$DOOR{"$self->{id}.$self->{floor}"} = 0;
}
Finally, at the bottom of the elevator loop there is a check on the
shared variable $FINISHED:
# simulation over?
{ lock $FINISHED; return if $FINISHED; }
Since the elevator threads are in an infinite loop the main thread
needs a way to tell them when the simulation is over. It uses the
shared variable $FINISHED for this purpose. I'll go into more
detail about why this is necessary later.
That's all there is to the Elevator class code. Elevators simply travel from floor to floor opening and closing doors in response to buttons being pushed by people.

