Gem #70: The Scope Locks Idiom

by Pat Rogers —AdaCore

Let's get started…


Like the classical “monitor” concept on which they are based, protected types provide mutually exclusive access to internal variables. Clients can only access these variables indirectly, by means of a procedural interface. This interface is very robust because mutually exclusive access is provided automatically: users cannot forget to acquire the underlying (logical) lock and cannot forget to release it, including when exceptions occur. As a result, encapsulating actions within protected operations is highly recommended.

However, applying a protected type and protected operations may not always be feasible. For example, consider an existing sequential program that makes calls to procedures and functions provided by a package. Inside the package are variables that are manipulated by the procedures and functions. If more than one task is now going to be calling these subprograms, the package-level variables will be subject to race conditions because they are (indirectly) shared among the calling tasks. Moving the procedures and functions into a protected object would provide the required mutual exclusion, but it would require changes to both the package and the callers. Additionally, the existing procedures and functions may perform potentially blocking operations, such as I/O, that are prohibited from within protected operations.

In such a case, the programmer must fall back to manually acquiring and releasing an explicit lock. The result is essentially that of using semaphores, a low-level and clearly much less robust approach. For example, to ensure serial execution of the exported operations, one could declare a lock at the package level, as shown below, and have each operation acquire and release it:

...
package body P is

   Mutex : Mutual_Exclusion;

   State : Integer := 0;

   procedure Operation_1 is
   begin
      Mutex.Seize;
      State := State + 1;  -- for example...
      Put_Line ("State is now" & State'Img);
      Mutex.Release;
   exception
      when others =>
         Mutex.Release;
         raise;
   end Operation_1;

   procedure Operation_2 is
   begin
      Mutex.Seize;
      State := State - 1;  -- for example...
      Put_Line ("State is now" & State'Img);
      Mutex.Release;
   exception
      when others =>
         Mutex.Release;
         raise;
   end Operation_2;

end P;

The type Mutual_Exclusion is actually a subtype of a binary semaphore abstraction available to users via the GNAT.Semaphores package:

  subtype Mutual_Exclusion is Binary_Semaphore
    (Initially_Available => True,
     Ceiling             => Default_Ceiling);

See package GNAT.Semaphores for the details. You can assume it is a protected type with classic semaphore semantics. We define a subtype to ensure that all such objects are initially available, as required when providing mutual exclusion.

Although we cannot eliminate the need for this lock, we can make the code more robust by automatically acquiring and releasing it using an object of a controlled type. Initialization will automatically acquire the lock and finalization will automatically release it, including both when an exception is raised and when the task is aborted. (C++ programmers may be familiar with this technique under the name "Resource Acquisition Is Initialization" (RAII).)

The idea is to define a limited controlled type that references a shared lock using a discriminant. Objects of the type are then declared within procedures and functions with a discriminant value designating the shared lock declared within the package. Such a type is called a “scope lock” because the elaboration of the enclosing declarative region – the scope – is sufficient to acquire the referenced lock. The subprogram’s sequence of statements will not execute until the lock is acquired, no matter how long that takes. When the procedure or function is done, for any reason, finalization will release the lock. The resulting user code is thus almost unchanged from the original sequential code:

...
package body P is

   Mutex : aliased Mutual_Exclusion;

   State : Integer := 0;

   procedure Operation_1 is
      S : Scope_Lock (Mutex’Access);
   begin
      State := State + 1;  -- for example...
      Put_Line ("State is now" & State'Img);
   end Operation_1;

   procedure Operation_2 is
      S : Scope_Lock (Mutex’Access);
   begin
      State := State - 1;  -- for example...
      Put_Line ("State is now" & State'Img);
   end Operation_2;

end P;

To define the Scope_Lock type, we declare it with a discriminant designating a Mutual_Exclusion object:

  type Scope_Lock (Lock : access Mutual_Exclusion) is
     tagged limited private;

In the private part the type is fully declared as a controlled type derived from Ada.Finalization.Limited_Controlled, as shown below. We hide the fact that the type will be controlled because Initialize and Finalize are never intended to be called manually.

   type Scope_Lock (Lock : access Mutual_Exclusion) is
      new Ada.Finalization.Limited_Controlled with null record;

   overriding procedure Initialize (This : in out Scope_Lock);
   overriding procedure Finalize   (This : in out Scope_Lock);

Each overridden procedure simply references the semaphore object designated by the formal parameter's discriminant:

  procedure Initialize (This : in out Scope_Lock) is
  begin
    This.Lock.Seize;
  end Initialize;

  procedure Finalize (This : in out Scope_Lock) is
  begin
    This.Lock.Release;
  end Finalize;

So as you can see, by combining controlled types with protected types one can make simple work of providing mutually exclusive access when a protected object is not an option. By taking advantage of the automatic calls to Initialize and Finalize, the resulting user code is much more robust and requires very little change.


About the Author

Pat Rogers has been a computing professional since 1975, primarily working on microprocessor-based real-time applications in Ada, C, C++ and other languages, including high-fidelity flight simulators and Supervisory Control and Data Acquisition (SCADA) systems controlling hazardous materials. Having first learned Ada in 1980, he was director of the Ada9X Laboratory for the U.S. Air Force’s Joint Advanced Strike Technology Program, Principle Investigator in distributed systems and fault tolerance research projects using Ada for the U.S. Air Force and Army, and Associate Director for Research at the NASA Software Engineering Research Center. He has B.S. and M.S. degrees in computer systems design and computer science from the University of Houston and a Ph.D. in computer science from the University of York, England. As a member of the Senior Technical Staff at AdaCore, he specializes in supporting real-time/embedded systems developers, creates and provides training courses, and is project leader and a developer of the GNATbench Eclipse plug-in for Ada. He also has a 3rd Dan black belt in Tae Kwon Do and is founder of the AdaCore club “The Wicked Uncles”.