Ada 95 Quality and Style Guide Chapter 6

Chapter 6: Concurrency - TOC - 6.2 COMMUNICATION

6.2.4 Unprotected Shared Variables

guideline

  • Use calls on protected subprograms or entries to pass data between tasks rather than unprotected shared variables.
  • Do not use unprotected shared variables as a task synchronization device.
  • Do not reference nonlocal variables in a guard .
  • If an unprotected shared variable is necessary, use the pragma Volatile or Atomic.

  • example

    This code will either print the same line more than once, fail to print some lines, or print garbled lines (part of one line followed by part of another) nondeterministically. This is because there is no synchronization or mutual exclusion between the task that reads a command and the one that acts on it. Without knowledge about their relative scheduling, the actual results cannot be predicted:

    -----------------------------------------------------------------------
    task body Line_Printer_Driver is
       ...
    begin
       loop
          Current_Line := Line_Buffer;
          -- send to device
       end loop;
    end Line_Printer_Driver;
    -----------------------------------------------------------------------
    task body Spool_Server is
       ...
    begin
       loop
          Disk_Read (Spool_File, Line_Buffer);
       end loop;
    end Spool_Server;
    -----------------------------------------------------------------------
    

    The following example shows a vending machine that dispenses the amount requested into an appropriately sized container. The guards reference the global variables Num_Requested and Item_Count, leading to a potential problem in the wrong amount being dispensed into an inappropriately sized container:

    Num_Requested : Natural;
    Item_Count    : Natural := 1000;
    task type Request_Manager (Personal_Limit : Natural := 1) is
       entry Make_Request (Num : Natural);
       entry Get_Container;
       entry Dispense;
    end Request_Manager;
    
    task body Request_Manager is
    begin
       loop
          select
             accept Make_Request (Num : Natural) do
                Num_Requested := Num;
             end Make_Request;
          or
             when Num_Requested < Item_Count =>
                accept Get_Container;
                ...
          or
             when Num_Requested < Item_Count =>
                accept Dispense do
                   if Num_Requested <= Personal_Limit then
                      Ada.Text_IO.Put_Line ("Please pick up items.");
                   else
                      Ada.Text_IO.Put_Line ("Sorry! Requesting too many items.");
                   end if;
                end Dispense;
          end select;
       end loop;
    end Request_Manager;
    R1 : Request_Manager (Personal_Limit => 10);
    R2 : Request_Manager (Personal_Limit => 2);  
    

    The interleaving of the execution of R1 and R2 can lead to Num_Requested being changed before the entry call to Dispense is accepted. Thus, R1 might receive fewer items than requested or R2's request might be bounced because the request manager thinks that what R2 is requesting exceeds R2's personal limit. By using the local variable, you will dispense the correct amount. Furthermore, by using the pragma Volatile (Ada Reference Manual 1995, §C.6), you ensure that the Item_Count is reevaluated when the guards are evaluated. Given that the variable Item_Count is not updated in this task body, the compiler might otherwise have optimized the code and not generated code to reevaluate Item_Count every time it is read:

    Item_Count : Natural := 1000;
    pragma Volatile (Item_Count);
    task body Request_Manager is
       Local_Num_Requested : Natural := 0;
    begin
       loop
          select
             accept Make_Request (Num : Natural) do
                Local_Num_Requested := Num;
             end Make_Request;
          or
             when Local_Num_Requested <= Personal_Limit =>
                accept Get_Container;
                ...
          or
             when Local_Num_Requested < Item_Count =>
                accept Dispense do
                   ... -- output appropriate message if couldn't service request
                end Dispense;
                Item_Count := Item_Count - Local_Num_Requested; 
          end select;
       end loop;
    end Request_Manager;
    

    rationale

    There are many techniques for protecting and synchronizing data access. You must program most of them yourself to use them. It is difficult to write a program that shares unprotected data correctly. If it is not done correctly, the reliability of the program suffers.

    Ada provides protected objects that encapsulate and provide synchronized access to protected data that is shared between tasks. Protected objects are expected to provide better performance than the rendezvous that usually requires introduction of an additional task to manage the shared data. The use of unprotected shared variables is more error-prone than the protected objects or rendezvous because the programmer must ensure that the unprotected shared variables are independently addressable and that the actions of reading or updating the same unprotected shared variable are sequential (Ada Reference Manual 1995, §9.10; Rationale 1995, §II.9).

    The first example above has a race condition requiring perfect interleaving of execution. This code can be made more reliable by introducing a flag that is set by Spool_Server and reset by Line_Printer_Driver. An if (condition flag) then delay ... else would be added to each task loop in order to ensure that the interleaving is satisfied. However, notice that this approach requires a delay and the associated rescheduling. Presumably, this rescheduling overhead is what is being avoided by not using the rendezvous.

    You might need to use an object in shared memory to communicate data between (Rationale 1995, §C.5):

    - Ada tasks
    - An Ada program and concurrent non-Ada processes
    - An Ada program and hardware devices

    If your environment supports the Systems Programming Annex (Ada Reference Manual 1995, Annex C), you should indicate whether loads and stores to the shared object must be indivisible. If you specify the pragma Atomic, make sure that the object meets the underlying hardware requirements for size and alignment.

    Multiple tasks sharing the predefined random number generator and certain input/output subprograms can lead to problems with unprotected updates to shared state. The Ada Reference Manual (1995, §A.5.2) points out the need for tasks to synchronize their access to the random number generators (packages Ada.Numerics.Float_Random and Ada.Numerics.Discrete_Random). See Guideline 7.7.5 for the I/O issue.


    < Previous Page Search Contents Index Next Page >
    1 2 3 4 5 6 7 8 9 10 11
    TOC TOC TOC TOC TOC TOC TOC TOC TOC TOC TOC
    Appendix References Bibliography