Previous

Contents

Next

Chapter 19:
Multitasking

Tell me your tasks in order.
— Dylan Thomas, Under Milk Wood


19.1 Active objects
19.2 Task types
19.3 Communicating with tasks
19.4 More about select statements
19.5 Transferring data during a rendezvous
19.6 Sharing data between tasks
19.7 An active spreadsheet
Exercises

19.1 Active objects

The objects you’ve met so far (spreadsheet cells, diary appointments and so on) are all essentially passive objects. They are acted upon by subprograms called (ultimately) by the main program; they don’t do anything of their own accord, only when asked to do so. However, active objects are often useful as well. Consider a type of spreadsheet cell that continually updates itself with constantly changing prices obtained online from the stock market. The current design would require the cell to obtain the latest prices whenever the spreadsheet was recalculated. The main program could recalculate the spreadsheet repeatedly if it was otherwise idle, but that puts the responsibility for recalculating on the program rather than on the object which requires it, in this case a particular type of spreadsheet cell. In a spreadsheet with no cells of this type, the continuous recalculating is a waste of resources. An active spreadsheet cell would resolve the problem by operating at the same time as (in parallel with) the rest of the program and requesting the spreadsheet to do a recalculation whenever its value changed. That way the program needn’t know anything about how the spreadsheet works or what type of cells it might contain and would have no extra responsibilities for managing the spreadsheet.

Ada allows you to define tasks which are executed independently. The program itself is executed by an environment task, and this is allowed to create further tasks which are executed in parallel with all the other tasks in the program. You could define a new type of spreadsheet cell which contained a task. As soon as such a cell was created, the task associated with it would begin executing in parallel with the rest of the program. Active cells like this would be able to change their values in response to external conditions and then call on the spreadsheet to recalculate itself. The spreadsheet itself could be an active object which waits for recalculation requests and then recalculates and displays itself, in parallel with everything else that’s happening.

Note that this feature of Ada is heavily dependent on the underlying operating system; an operating system like MS-DOS has no multitasking capabilities (and, indeed, is positively hostile to multitasking) so that implementations of Ada for MS-DOS systems can only provide multitasking capabilities poorly, if at all. Fortunately the situation is changing, and ’real’ operating systems like Unix, OS/2 and Windows/NT are driving out the ‘toy’ systems which sprang up in the infancy of microcomputers and have stayed with us like some debilitating disease ever since.


19.2 Task types

Like packages, Ada tasks are defined in two parts, a specification part and a body part. Unlike packages, they are not compilation units; they cannot be compiled independently and added to the library. Instead, they must be declared inside a package or subprogram. When declared in a package, the specification goes in the package specification and the body goes in the package body; when declared in a subprogram, both specification and body must be defined in the subprogram.

The task specification defines a task type. The simplest form that this can take looks like this:

    task type Repeat;

This defines a type called Repeat; you can then declare as many tasks of this type as you require:

    A, B : Repeat;                          -- two tasks
    C    : array (1..100) of Repeat;        -- an array of 100 tasks

These tasks will be started at the end of the declaration section where they were created, i.e. immediately before the first statement in the block where they are declared:

    declare
        A, B : Repeat;
        C    : array (1..100) of Repeat;
    begin                    -- all 102 tasks are started at this point
        ...
    end;                     -- wait here for all 102 tasks to end

When they are started, they will each execute a copy of the task body in parallel with everything else that’s happening. The tasks are local to the block, so they cease to exist at the end of the block. When the task executing the block reaches the end of the block, it will have to wait for all 102 tasks to end before it can proceed. The task executing the block is said to be the master of the tasks created within it, and the tasks within the block are known as the dependents of their master task.

The task body will normally contain a loop. Here’s a simple example:

    task body Repeat is
    begin
        for I in 1..5 loop
            Put_Line ("Hello!");
            delay 2.0;
        end loop;
    end Repeat;

This will display the message ‘Hello!’ five times before terminating. After the message is displayed, the task will be delayed for two seconds before continuing by the delay statement:

    delay 2.0;

Here 2.0 is a value of the standard fixed point type Duration which specifies the length of the delay in seconds. You can also delay until a particular time like this:

    delay until Ada.Calendar.Time_Of (Day=>25, Month=>12, Year=>1999);

This statement will cause the task which executes it to wait until Christmas 1999. The time is specified as a value of type Ada.Calendar.Time.

Task types are limited types, so you can’t assign tasks to one another or compare them; this also means that any type which contains a task as a component must also be a limited type. If you only want a single task of a particular type, you can declare the task specification like this:

    task Repeat;

The task type is now anonymous, and Repeat is the only object belonging to this anonymous type. In other words, this is effectively like writing the following:

    task type ???;    -- ??? is the "name" of the anonymous task type
    Repeat : ???;     -- declare one object of this type

Tasks can also have discriminants, which can be a useful feature for providing initial values:

    task type Repeat (Count : Natural);

    task body Repeat is
    begin
        for I in 1..Count loop        -- discriminant controls loop length
            Put_Line (Integer'Image(Count) & " Hello!");
            delay 2.0;
        end loop;
    end Repeat;

    A : Repeat (Count => 10);         -- this task says hello 10 times

Notice that the discriminant is only specified in the task specification and not in the body, but it can still be referred to from within the body. Also, since task types are limited types, access discriminants are perfectly acceptable.


19.3 Communicating with tasks

Usually a task will be expected to do something more complex than just displaying a message over and over. It will normally be necessary for tasks to communicate with each other; for example, a task in a spreadsheet cell will need to be asked what the cell value is from time to time, or a spreadsheet may need to be asked to recalculate itself. In such cases the task specification needs to be expanded to list the services that a task can provide. Here’s the specification of a spreadsheet task which allows other tasks to ask it to do a recalculation:

    task type Spreadsheet_Task is
        entry Recalculate;
    end Spreadsheet_Task;

    Sheet : Spreadsheet_Task;        -- declare a Spreadsheet_Task

This task type provides an entry specification which another task can call just like a procedure; for example, a task can ask Sheet to recalculate with an entry call like this:

    Sheet.Recalculate;

The task body has to provide a way of servicing calls to its entries. This is done using an accept statement:

    task body Spreadsheet_Task is
    begin
        loop
            accept Recalculate;
            Do_Recalculation;
        end loop;
    end Spreadsheet_Task;

When the task body starts executing, it will wait at the accept statement until an entry call to Recalculate is made. It will then call a procedure Do_Recalculation to perform the recalculation and go round the loop again to wait for the next call to Recalculate. If another task calls Recalculate before the spreadsheet task has got back to the accept statement again, the calling task is forced to wait. Thus the calling task and the one being called will wait for each other until they are both ready, which is when the caller is waiting for its entry call to be accepted and the one being called is waiting at the accept statement. This synchronisation of the two tasks is known as a rendezvous.

The task body above has one major problem; since it’s an infinite loop there’s no way to get it to terminate, so the master task won’t be able to terminate either; it’ll end up waiting forever at the end of the block where the spreadsheet task was declared. One way to get around this is to abort the task with an abort statement:

    abort Sheet;

This will force the task and any dependent tasks it might have to terminate. However, this is rather drastic since the spreadsheet might be halfway through a recalculation at the time. A better way would be to add another entry to allow the master task to ask it to shut down in an orderly manner:

    task type Spreadsheet_Task is
        entry Recalculate;
        entry Shutdown;
    end Spreadsheet_Task;

Now the task body needs to be able to respond to calls to either entry. It’s no good accepting them one after the other in a loop since this will force the entries to be called alternately. One solution would be to test if any calls are pending before accepting them. You can do this using the 'Count attribute for an entry which gives the number of pending calls for that entry:

    task body Spreadsheet_Task is
    begin
        loop
            if Recalculate'Count > 0 then
                accept Recalculate;
                Do_Recalculation;
            elsif Shutdown'Count > 0 then
                accept Shutdown;
                exit;
            end if;
        end loop;
    end Spreadsheet_Task;

However, this isn’t particularly reliable. As you’ll see later, tasks can choose to abandon entry calls if they aren’t responded to within a certain time period, and this means that even if Recalculate'Count is non-zero, by the time you execute the accept statement for Recalculate the calling task might have timed out and abandoned its call, in which case you’ll be stuck at the accept statement until some other task calls Recalculate. And if that never happens, you’ll never be able to accept a call to Shutdown. The correct solution to this is to put the calls inside a select statement:

    task body Spreadsheet_Task is
    begin
        loop
            select
                accept Recalculate;
                Do_Recalculation;
            or
                accept Shutdown;
                exit;
            end select;
        end loop;
    end Spreadsheet_Task;

This select statement contains two accept alternatives which must each be headed by an accept statement. It will wait until one of the entries named in the accept statements is called, and it will then execute the appropriate alternative. If calls to both entries are already pending, one will be accepted non-deterministically. The select statement ends after the chosen alternative has been executed; if Recalculate was called, the task will then go around the loop again and wait for another entry call, but if Shutdown was called it will exit from the loop and terminate. Note that the task will not respond to calls to Shutdown if a recalculation is in progress; it will only respond when it’s waiting for an entry call in the select statement, which will happen after the call to Recalculate finishes and the loop is repeated.

This solution requires the master task to call Shutdown explicitly when it wants to terminate the task. The disadvantage with this approach is that it’s possible to forget to call Shutdown. A better solution is to add a terminate alternative to the select statement:

    task body Spreadsheet_Task is
    begin
        loop
            select
                accept Recalculate;
                Do_Recalculation;
            or
                accept Shutdown;
                exit;
            or
                terminate;
            end select;
        end loop;
    end Spreadsheet_Task;

The terminate alternative must be the last one in a select statement, and it can’t contain anything except a terminate statement like the one shown above. When the master task gets to the end of the block where the spreadsheet was declared, the spreadsheet task will terminate the next time that the select statement is executed (or immediately, if the task is already waiting in the select statement). This means that the master doesn’t have to do anything special to terminate the task, but it can still call Shutdown if it wants to terminate the task before the end of the block where it was declared. Note that once a task has terminated, you’ll be rewarded with a Tasking_Error exception if you try to call any of its entries.


19.4 More about select statements

Select statements can also be used for entry calls. If a calling task isn’t prepared to wait for a rendezvous, it can use a select statement with an else alternative like this:

    select
        Sheet.Recalculate;
    else
        Put_Line ("Sheet is busy -- giving up");
    end select;

In this case, if Sheet is not able to accept the entry call to Recalculate immediately, the entry call will be abandoned and the else alternative will be executed. If the calling task is willing to wait for a limited time, it can use a select statement with a delay alternative like this:

    select
        Sheet.Recalculate;
    or
        delay 5.0;
        Put_Line ("Sheet has been busy for 5 seconds -- giving up");
    end select;

If the entry call is not accepted within the time specified in the delay statement (five seconds in this case), it’s abandoned and the delay alternative is executed instead. A delay until statement can always be used instead of a delay statement:

    select
        Sheet.Recalculate;
    or
        delay until Christmas;
        Put_Line ("Sheet has been busy for ages -- giving up");
    end select;

You can also set an upper limit on the time it takes to process an entry call:

    select
        delay 5.0;
        Put_Line ("Sheet not recalculated yet -- recalculation abandoned");
    then abort
        Sheet.Recalculate;
    end select;

This starts evaluating the statements between then abort and end select, which in this case is a call to Sheet.Recalculate. If the delay specified by the delay (or delay until) statement after select expires before this completes, the call to Sheet.Recalculate is aborted (as if by an abort statement) and the message ‘Sheet not recalculated yet -- recalculation abandoned’ will be displayed. You aren’t restricted to using this in connection with multitasking; for example, you could use it to abort lengthy calculations where the total execution time is important (or where the calculation might diverge to give potentially infinite execution times):

    select
        delay 5.0;
        Put_Line ("Horribly long calculation abandoned");
    then abort
        Horribly_Long_And_Possibly_Divergent_Calculation;
    end select;

A select statement inside a task body can also have an else alternative or one or more delay alternatives instead of a terminate alternative. You must also have at least one accept alternative. An else alternative is activated if none of the accept statements have a pending entry call; a delay alternative is activated if none of the accept statements accept an entry call within the time specified in the delay statement. These three possibilities (else, delay and terminate) are mutually exclusive; you cannot have a delay alternative as well as a terminate alternative, for example.


19.5 Transferring data during a rendezvous

It may also be necessary to transfer data between tasks during a rendezvous. For example, a spreadsheet cell might need an entry to allow other tasks to get its value. To allow this to happen, task entries can also have parameters just like procedures:

    task type Counter_Task is
        entry Get (Value : out Integer);
    end Counter_Task;

    task body Counter_Task is
        V : Integer := 0;
    begin
        loop
            select
                accept Get (Value : out Integer) do
                    Value := V;
                    V     := V + 1;
                end Get;
            or
                terminate;
            end select;
        end loop;
    end Counter_Task;

The accept statement in this task body acts basically like a procedure which is invoked by the entry call. It can even contain return statements just like a procedure. When the entry call is accepted, any in parameters are transferred from the caller. The body of the accept statement is then executed, and at the end any out parameters are transferred back to the caller. The rendezvous is then complete, and the caller is allowed to continue. In this case the task will generate ever-increasing integer values each time Get is called.

You might not always be willing to accept an entry call. Consider this task which contains a stack which other tasks can push data onto or pop items off:

    task type Stack_Manager is
        entry Push (Item : in Integer);
        entry Pop (Item : out Integer);
    end Stack_Manager;

    task body Stack_Manager is
        package Int_Stacks is new JE.Stacks (Integer);
        Stack : Int_Stacks.Stack_Type;
    begin
        loop
            select
                accept Push (Item : in Integer) do
                    Int_Stacks.Push (Stack,Item);
                end Push;
            or
                accept Pop (Item : out Integer) do
                    Int_Stacks.Pop (Stack,Item);
                end Pop;
            or
                terminate;
            end select;
        end loop;
    end Stack_Manager;

An exception will be raised if an attempt is made to call Pop on an empty stack. Note that if an exception occurs during a rendezvous, the exception will be raised in the calling task as well as the one being called. To prevent this happening, we can add a guard to the accept statement for Pop like this:

    when not Int_Stacks.Empty (Stack) =>
        accept Pop (Item : out Integer) do ...

A guarded accept statement can only be activated when the condition specified in the guard is True, which in this case means that Pop can only be called when the stack is not empty. Any task which calls Pop when the stack is empty will be forced to wait until another task calls Push. As soon as Push has been called, the stack will no longer be empty so that the next time the select statement is executed the pending call to Pop will immediately be accepted.

Guarded entries can also be useful for aborting actions in a select ... then abort construct. A select ... then abort construct is governed by a triggering alternative (the first statement after select) which must be an entry call or a delay statement. When the triggering alternative is activated (the entry call is accepted or the delay expires) the abortable part between then abort and end select is aborted as if by an abort statement:

    select
        User.Interrupt;
        Put_Line ("Horribly long calculation interrupted by user");
    then abort
        Horribly_Long_And_Possibly_Divergent_Calculation;
    end select;

In this case, if the call to User.Interrupt (the Interrupt entry of the task User) is ever accepted, the horribly long calculation will be aborted. If User.Interrupt has a guard, this means that when the guard condition becomes True the horribly long calculation between then abort and end select will be aborted.


19.6 Sharing data between tasks

Using a task to allow multiple tasks to access a common stack like the example above is a very elaborate and expensive way of sharing data. It means there has to be an extra task to manage the stack on behalf of the other tasks which want to use it, and a rendezvous is required to access the stack. A rendezvous is a relatively lengthy operation, so it adds quite a large overhead to what would otherwise be a fairly simple procedure call.

The stack could of course be declared in the same scope as the task types that need to access it, but this is extremely risky. Consider the following section of code from chapter 13:

    procedure Pop (Stack : in out Stack_Type;
                   Item  : out Item_Type) is
    begin
        Item      := Stack.Body(Stack.Top);    -- 1
        Stack.Top := Stack.Top - 1;            -- 2
    exception
        when Constraint_Error =>
            raise Stack_Underflow;
    end Pop;

This shows how Pop is implemented using an array. In an environment where only one task is executing this code, it’s perfectly safe. If more than one task is executing it simultaneously, both tasks might execute statement 1 at the same time so that they will both be given a copy of the same item. When they execute statement 2, Stack.Top might be decremented twice or both tasks might retrieve the same value for Stack.Top, subtract 1 from it and then store the result in Stack.Top so that Stack.Top will appear to have been decremented only once.

In other words, the result will be completely unpredictable since it depends on the precise timing relationship between the two tasks. Unpredictability on this scale is rarely a good property for computer systems to have. The moral of the story is that tasks should never access external data; they should only ever access their own local objects.

To get around this problem, Ada allows data to be encapsulated in a protected record which guarantees that this sort of situation can’t arise. A protected record is a passive data type rather than an active type like a task, so the costs of a rendezvous and the scheduling of an extra task are avoided. Protected records are divided into a specification and a body, just like tasks. The specification contains a visible part which declares a set of functions, procedures and entries that tasks are allowed to call as well as a private part which contains the data to be protected. Here’s a protected record which encapsulates a stack of integers:

    protected type Shared_Stack_Type is
        procedure Push (Item : in Integer);
        entry Pop (Item : out Integer);
        function Top return Integer;
        function Size return Natural;
        function Empty return Boolean;
    private
        package Int_Stacks is new JE.Stacks (Integer);
        Stack : Int_Stacks.Stack_Type;
    end Shared_Stack_Type;

    Stack : Shared_Stack_Type;    -- declare an instance of Shared_Stack_Type

As with tasks, you can declare a single protected record of an anonymous type by leaving out the word type:

    protected Shared_Stack_Type is ... ; -- same as: protected type ???;
                                         --          Shared_Stack_Type : ???;

The body provides the implementations of the functions, procedures and entries declared in the specification. The difference between the three types of operation is that functions are only allowed to read the values of private data items; such items appear to a function as if they were constants and the function is unable to alter them. Since it’s safe for several tasks to read the same data at the same time, multiple tasks are allowed to execute functions in a protected object at the same time. Procedures and entries are allowed to alter the private data, so a task can’t call any protected operations while another task is executing a procedure or entry call. The difference between a procedure and an entry is that entries have guards which act like the guards on accept statements; an entry can only be executed when its guard is True, and any task which calls an entry whose guard is False will be suspended until the guard becomes True (at which point the entry call can then be executed).

In the protected type Shared_Stack_Type, there are three functions (Top, Size and Empty) which don’t affect the private stack it contains. Tasks will be able to call these functions as long as no procedure or entry call is in progress; if there is already a procedure or entry call in progress, the task calling the function will not be allowed to proceed until the active call finishes executing. There is one procedure (Push); any task calling Push will have to wait until any other active calls have finished executing. There is one entry (Pop); any task calling Pop will have to wait, not only until any other active calls have finished executing, but also until the entry guard is True.

Here’s the protected body. The guard condition for the entry Pop is specified after the parameter list between when and is:

    protected body Shared_Stack_Type is

        procedure Push (Item : in Integer) is
        begin
            Int_Stacks.Push (Stack,Item);
        end Push;

        entry Pop (Item : out Integer)
            when not Int_Stacks.Empty (Stack) is
        begin
            Int_Stacks.Pop (Stack,Item);
        end Pop;

        function Top return Integer is
        begin
            return Int_Stacks.Top (Stack);
        end Top;

        function Size return Natural is
        begin
            return Int_Stacks.Size (Stack);
        end Size;

        function Empty return Boolean is
        begin
            return Int_Stacks.Empty (Stack);
        end Empty;

    end Shared_Stack_Type;

So, as many tasks as want to can simultaneously inspect the top item on the stack, find out the size of the stack or test if it’s empty as long as no-one’s pushing an item onto the stack or popping one off it. Popping an item off the stack is only allowed if the stack isn’t empty; if it is empty the caller task will have to wait until another task calls Push. Calls to Push and Pop will only go ahead when the protected record isn’t in use by any other task.


19.7 An active spreadsheet

To illustrate all this in action, let’s consider a modification of the spreadsheet in the previous chapter which will allow active cells to be included in the spreadsheet. A simple example will be a cell containing a task which changes its value every five seconds:

    task type Counter_Task (Sheet : access Speadsheet_Type'Class) is
        entry Get (Value : out Integer);
        entry Stop;
    end Counter_Task;

This task has an access discriminant which will be set to point to the spreadsheet containing the task. The body of this task will look like this:

    task body Counter_Task is
        type Count_Type is mod 10000;
        Count       : Count_Type        := Count_Type'First;
        Update_Time : Ada.Calendar.Time := Ada.Calendar.Clock + 5.0;
    begin
        loop
            select
                accept Get (Value : out Integer) do
                    Value := Integer(Count);
                end Get;
            or
                accept Stop;
                exit;
            or
                delay until Update_Time;
                Update_Time := Update_Time + 5.0;
                Count       := Count + 1;
                Change (Sheet.all);
            end select;
        end loop;
    end Counter_Task;

All this does is to sit in a loop accepting entry calls to Get or Stop or delaying until the update time is reached. Note that a delay statement which said ‘delay 5.0’ wouldn’t be any good; the delay would then be five seconds plus the time it took to get around the loop and back to the delay statement again; although the time it takes to get around the loop may be very small it will gradually accumulate. This would be unacceptable in a time-critical application.

Get returns the current counter value and Stop terminates the task. If neither of these is called before the update time is reached, the delay will expire with the result that the update time and the count are both updated and the spreadsheet is notified that a change has taken place. A modular type is used for Count so that when it reaches its maximum value it will go back to zero rather than raising a Constraint_Error.

A problem with this is that calling Change will update an unprotected data item (the Dirty flag in the spreadsheet); we could derive a new type of spreadsheet which incorporates a protected record to get around this if it’s a problem:

    protected type Shared_Flag_Type is
        function State return Boolean;
        procedure Set;
        procedure Clear;
    private
        State_Flag : Boolean := False;
    end Shared_Flag_Type;

    protected body Shared_Flag_Type is
        function State return Boolean is
        begin
            return State_Flag;
        end State;

        procedure Set is
        begin
            State_Flag := True;
        end Set;

        procedure Clear is
        begin
            State_Flag := False;
        end Clear;
    end Shared_Flag_Type;

    type Active_Spreadsheet_Type is
        abstract new Spreadsheet_Type with
        record
            Modified : Shared_Flag_Type;
        end record;

The primitive operations Change, Updated and Changed will need overriding to use the operations of Shared_Flag_Type:

    procedure Change (Sheet : in out Active_Spreadsheet_Type) is
    begin
        Sheet.Modified.Set;
    end Change;

    procedure Updated (Sheet : in out Active_Spreadsheet_Type) is
    begin
        Sheet.Modified.Clear;
    end Updated;

    function Changed (Sheet : Active_Spreadsheet_Type)
                                    return Boolean is
    begin
        return Sheet.Modified.State;
    end Changed;

The next thing we need is a derived Cell_Type to hold an instance of the counter task:

    type Counting_Cell_Type (Sheet : access Spreadsheet_Type'Class) is
        new Cell_Type with
        record
            Counter : Counter_Task(Sheet);
        end record;

Since the parent type Cell_Type is derived from Limited_Controlled, we can override Finalize to stop the counter task:

    procedure Finalize (Object : in out Counting_Cell_Type) is
    begin
        Object.Counter.Stop;
    end Finalize;

The primitive operations Contents, Cell_Value and Num_Value and Evalute which were inherited from Cell_Type will all need to be overridden. Contents can be defined to return a string identifying the cell as a five-second counter:

    function Contents (Cell : Counting_Cell_Type)
                            return String is
    begin
        return "<5-second counter>";
    end Contents;

Text_Value can be implemented by returning the current value of the cell as a string. The current value can be got by calling Num_Value:

    function Text_Value (Cell : Counting_Cell_Type)
                            return String is
    begin
        return Integer'Image(Num_Value(Cell));
    end Text_Value;

Num_Value needs to rendezvous with the task to get the current value of the counter:

    function Num_Value (Cell : Counting_Cell_Type)
                            return Integer is
        I : Integer;
    begin
        Cell.Counter.Get (I);
        return I;
    end Value;

Evaluate just needs to set the cell state to Defined since the value of a counting cell is always well-defined:

    procedure Evaluate (Cell : in out Counting_Cell_Type) is
    begin
        Cell.State := Defined;
    end Evaluate;

Finally, a constructor function is needed, just like the constructors for String_Cell_Type and Formula_Cell_Type:

    function Counting_Cell (Sheet : access Spreadsheet_Type'Class)
                           return Cell_Access is
        Cell : Cell_Access := new Counting_Cell_Type (Sheet);
    begin
        return Cell;
    end Counting_Cell;

The view package will also need some modification. Obviously the declaration of Sheet_Type must be changed to use an Active_Spreadsheet_Type instead of an ordinary Spreadsheet_Type. If you’re waiting for a command and the spreadsheet gets updated, you’ll need to redisplay the spreadsheet. One way to do this is to supply the spreadsheet as a parameter to Next_Command and get Next_Command to redisplay it if it’s been updated. You can use the procedure Get_Immediate from Ada.Text_IO to do this:

    procedure Get_Immediate (Item      : out Character;
                             Available : out Boolean);

This procedure doesn’t wait for a key to be pressed; if a key has been pressed it returns it in Item and sets Available to True, but if not it just returns immediately with Available set to False. Here’s how it could be used:

    function Next_Command return Command_Type is
        Command   : Character;
        Available : Boolean;
    begin
        loop
            New_Line;
            Put ("(M)odify, (D)isplay or (Q)uit: ");
            loop
                Get_Immediate (Command, Available);
                exit when Available;
                if Changed(Sheet.Innards) then
                    Display (Sheet.Innards);
                    New_Line;
                    Put ("(M)odify, (D)isplay or (Q)uit: ");
                end if;
            end loop;
            Skip_Line;
            case Command is
                ...        -- as before
            end case;
        end loop;
    end Next_Command;

As long as no key is pressed, the inner loop will keep checking the state of the spreadsheet and redisplay it if necessary, but as soon as a key is pressed it’ll cause the inner loop to exit and command processing will then be done as normal.

This isn’t an ideal solution; probably the best thing would be to put the spreadsheet inside a task in the view package and get the task to monitor the spreadsheet for changes and redraw it whenever necessary, rather than depending on Next_Command to do all the work. However, this depends on your ability to write to particular places on the screen without affecting the input cursor, so this would be a very system-dependent solution.

The final step is to change Modify so that it can create counting cells. I’ll use the character ‘#’ to create counting cells:

    procedure Modify (Sheet : in out Sheet_Type) is
        Name      : String(1..10);
        Name_Size : Natural
        Line      : String(1..50);
        Line_Size : Natural;
        Which     : Cell_Access;
    begin
        ...        -- as before
        Put ("Enter new value: ");
        Get_Line (Line, Line_Size);
        if Line_Size > 0 then                 -- new value entered
            case Line(1) is
                when '#' =>                   -- counting cell
                    Insert (Sheet.Innards, Name(1..Name_Size),
                     Counting_Cell (Sheet.Innards'Access));
                when '.' =>                   -- empty cell
                    ...        -- as before
                when '"' =>                   -- string cell
                    ...        -- as before
                when others =>                -- formula cell
                    ...        -- as before
            end case;
            Display (Sheet);
        end if;
    end Modify;

Apart from anything else, this shows how easy it can be to modify the existing spreadsheet. The object-oriented approach accommodates new types of cells since the spreadsheet can handle any type derived from Cell_Type, so as long as the services provided by Cell_Type are adequate we can override them to provide whatever behaviour we want without affecting the spreadsheet itself; similarly the spreadsheet itself can be modified by overriding any operations whose implementation needs to change. In this case I’ve added a protected record; since all that it’s used for is to protect a single Boolean variable it’s unlikely to make any practical difference, but it shows how important it is that the original spreadsheet provided a set of primitive operations for manipulating its own internal state rather than assuming that the internal state would always be managed in the same way.


Exercises

19.1 Modify Counting_Cell_Type so that you can specify the delay rather than having a fixed five-second delay.

19.2 Modify the spreadsheet program in this chapter to use an abortable select statement instead of calling Get_Immediate to get commands from the user. The select statement should call Get or Get_Line and abort the call if it has not completed within one second, redisplaying the spreadsheet if it has changed.

19.3 Modify the guessing game program from exercise 5.1 to impose a maximum time limit within which the user must guess the secret value.

19.4 Define a bank account type similar to that described in exercise 14.2 which is based on a protected record to make it safe for use in a multitasking program (so that multiple tasks can ‘simultaneously’ deposit and withdraw money). Test it using two tasks which deposit and withdraw amounts of money at random intervals; check that the totals deposited and withdrawn by each task match up with the final balance of the account.



Previous

Contents

Next

This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright © John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $