Resource Acquisition Is Initialization (RAII)

Motivation

In order for the expected semantics to be obtained, some types require clients to follow a specific protocol when calling the type's operations. Furthermore, failing to follow the protocol can cause system-wide ill effects.

For example, concurrency abstractions such as mutexes provide the mutually exclusive access necessary to prevent the race conditions that arise when competing concurrent threads access shared resources. These mutex objects must be 1) both acquired and released, 2) by every thread accessing that shared resource, 3) at the right places in the source code, and 4) in the proper order. Failure to acquire the mutex prior to accessing the shared resource leads to race conditions, and failure to release it can lead to deadlocks. Ensuring the mutex is released is complicated by the possibility of exceptions raised after the lock is acquired.

Although concurrency is a prime example, the issue is general in nature. We will continue with the concurrency context for the sake of discussion.

Like the classic monitor concept ([1], [2], [3]) on which they are based, Ada defines a protected object (PO) as a concurrency construct that is higher-level and more robust than mutexes and semaphores. Those advantages accrue because the bodies of the protected operations are only responsible for implementing the functional requirements. The underlying run-time library is responsible for implementing the mutually exclusive access, and also thread management. As a result, the source code is much simpler and is robust even in the face of exceptions.

(In the works cited above, Hoare's contribution was equally important, but Hansen's contributions were reified in Concurrent Pascal, a concrete programming language.)

However, a protected object is not always appropriate. Consider an existing sequential program that makes calls to visible procedures provided by a package:

package P is

   procedure Operation_1;

   procedure Operation_2;

   --  ...

end P;

Inside the package body are one or more state variables that are manipulated by the procedures (i.e., as in an Abstract Data Machine):

with Ada.Text_IO;  use Ada.Text_IO;

package body P is

   State : Integer := 0;

   procedure Operation_1 is
   begin
      State := State + 1;  -- for example...
      Put_Line ("State is now " & State'Image);
   end Operation_1;

   procedure Operation_2 is
   begin
      State := State - 1;  -- for example...
      Put_Line ("State is now " & State'Image);
   end Operation_2;

   --  ...

end P;

This design is reasonable in a strictly sequential caller context. But if new application requirements are such that multiple tasks will be calling these procedures asynchronously, there is a problem. The package-level variable State will be subject to race conditions because it is (indirectly) shared among the calling tasks. Race conditions tend to be Heisenbugs because they are timing-dependent, so they can be exceedingly difficult to identify and expensive to debug.

In response to the new requirements, we could declare a protected object within the package body and move the declaration of State into that PO. In addition, we would declare two protected procedures corresponding to Operation_1 and Operations_2. The two new protected procedure bodies would do what the original procedures did, including accessing and updating State. The original procedures — still presented to clients — would now call these new protected procedures:

with Ada.Text_IO;  use Ada.Text_IO;

package body P is

   protected Threadsafe is
      procedure Operation_1;
      procedure Operation_2;
   private
      State : Integer := 0;
   end Threadsafe;

   protected body Threadsafe is

      procedure Operation_1 is
      begin
         State := State + 1;  -- for example...
         Put_Line ("State is now " & State'Image);
      end Operation_1;

      procedure Operation_2 is
      begin
         State := State - 1;  -- for example...
         Put_Line ("State is now " & State'Image);
      end Operation_2;

   end Threadsafe;

   procedure Operation_1 is
   begin
      Threadsafe.Operation_1;
   end Operation_1;

   procedure Operation_2 is
   begin
      Threadsafe.Operation_2;
   end Operation_2;

   --  ...

end P;

As a result, there can be no race conditions on State, and the source code is both simple and robust.

We could put the PO in the package spec and then clients could call the protected object's operations directly, but changing all the clients could be expensive.

However, this new design is not portable because the two Threadsafe protected procedure bodies both call a potentially blocking operation, in this case Ada.Text_IO.Put_Line. Erroneous execution is the result. It might work as intended when executed, or it might do something else, or, if detected, Program_Error will be raised. On a run-time library built on top of an operating system, it may work as intended because the OS may provide thread locking mechanisms that the run-time library can use. In that case a blocking operation just suspends the caller thread's execution temporarily without releasing the PO lock. Although the blocking operation would allow some other caller task to be dispatched, no other caller could acquire that same PO lock, so race conditions are prevented within that PO. When the blocking operation returns, the protected procedure body can continue executing, still holding the lock. However, on a run-time library that does not use locks for mutual exclusion — it can use priorities, in particular — another caller to that same PO could access the enclosed variables while the first caller is blocked, thus breaking the mutually exclusive access guarantee.

Calling an I/O operation is not all that strange here, and those are not the only potentially blocking operation defined by the language.

Note that moving the calls to Put_Line out of the PO procedure bodies, back to the regular procedure bodies that call those PO procedures, would solve the portability problem but would not work functionally. There would be no guarantee that, during execution, the call to Put_Line would immediately follow the execution of the protected procedure called immediately before it in the source code. Hence the printed text might not reflect the current value of the State variable.

As a consequence, we must fall back to manually acquiring and releasing an explicit lock. For example, we could declare a lock object at the package level, as shown below, and have each operation acquire and release it:

with GNAT.Semaphores;  use GNAT.Semaphores;
with Ada.Text_IO;      use Ada.Text_IO;

package body P is

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

   Lock : Mutual_Exclusion;

   State : Integer := 0;

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

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

end P;

The subtype Mutual_Exclusion is just a binary semaphore with the discriminant set so that any object of the subtype is initially available. You can assume it is a protected type with classic binary semaphore semantics. See package GNAT.Semaphores for the details. The ceiling discriminant isn't important here, but we must set them all if we set any of them.

This design works, but the resulting code is clearly more complex and less robust than the PO approach.

Solution

Our solution uses an explicit global lock (a mutex), as above, but reintroduces automatic lock acquisition and release.

To achieve that automation, we leverage the language-defined object lifetime rules. These rules specify that an object is initialized when it is created and finalized when it is about to be destroyed. Initialization and finalization may be null operations, and thus absent from the object code, but application developers can define explicit initialization and finalization operations. When defined, these operations are called automatically by the underlying implementation, during the object's lifetime.

We will use the object initialization operation to seize the global lock and the object finalization operation to release it. The object lifetime rules will ensure that the lock's operations are called at the necessary times, thereby providing the required mutually exclusive access. In addition, the rules will ensure that the lock will be released even if an exception is raised in the bracketed application code.

Developers may be familiar with this approach under the name Resource Acquisition Is Initialization. Another name for this technique is Scope-Bound Resource Management because of the initialization and finalization steps invoked upon scope entry and exit.

Therefore, we will create a new type with user-defined initialization and finalization operations. We name this new type Lock_Manager because the type provides a wrapper for locks, rather than being a lock directly. Object creation and destruction will invoke the initialization and finalization routines, automatically.

Because they are wrappers for locks, each object of this type will reference a distinct lock object so that the initialization and finalization operations can manipulate that lock object. We use an access discriminant to designate that lock. By doing so, we decouple the new type from the specific lock, and thus from the application code. Otherwise, the new facility would not be reusable.

The resulting relationship between the global shared lock and the local object will be as follows:

Lock : Mutual_Exclusion;

procedure Op is
   LM : Lock_Manager (<pointer to Lock>)
   --  <initialization automatically called for LM>
begin
   --  ... sequence of statements for Op
   --  <finalization called for LM>
end Op;

The language rules specify that a subprogram's local declarative part is elaborated prior to the execution of that subprogram's sequence of statements. During that elaboration, objects are created and initialized. The object creation for LM precedes the sequence of statements in the procedure body for Op, so the designated lock will be acquired prior to the shared resource use within that body.

Similarly, the rules specify that finalization occurs when an object is about to cease to exist, in this case because the local object LM goes out of scope. That won't happen until the end of the sequence of statements is reached for Op, in the normal case, so finalization will ensure that the lock is released after any possible reference in Op's statement sequence. The run-time will also invoke finalization in the face of exceptions because exceptions also cause the scope to be exited.

To define the Lock_Manager type, we declare it in a separate package as a tagged limited private type with a discriminant designating a Mutual_Exclusion object:

type Lock_Manager (Lock : not null access Mutual_Exclusion) is
   tagged limited private;

We make it a limited type because copying doesn't make sense semantically for Lock_Manager objects.

In addition, during optimization the compiler is allowed to remove unreferenced objects of non-limited types. As you saw above in procedure Op, there will be no explicit references to the object LM, so making the type limited prevents that unwanted optimization.

Only controlled types support user-defined initialization and finalization operations (as of Ada 2022). Therefore, in the package 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 we don't intend Initialize and Finalize to be called manually by clients.

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

No additional record components are required, beyond the access discriminant.

Immediately following the type declaration, we declare overriding versions of the inherited procedures Initialize and Finalize:

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

These are the operations called automatically by the implementation.

The full package spec is as follows:

with Ada.Finalization;
with GNAT.Semaphores; use GNAT.Semaphores;

package Lock_Managers is

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

   type Lock_Manager (Lock : not null access Mutual_Exclusion) is
      tagged limited private;

private

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

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

end Lock_Managers;

The fact that there are no visible primitive operations tells the reader that this is a somewhat different ADT. The most useful thing a client can do with such a type is to declare objects, but that's exactly what we want.

Each overridden procedure simply references the lock designated by the formal parameter's Lock discriminant:

package body Lock_Managers is

   ----------------
   -- Initialize --
   ----------------

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

   --------------
   -- Finalize --
   --------------

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

end Lock_Managers;

The resulting user code is almost unchanged from the original sequential code:

with Ada.Text_IO;    use Ada.Text_IO;
with Lock_Managers;  use Lock_Managers;

package body P is

   State : Integer := 0;

   Lock : aliased Mutual_Exclusion;

   procedure Operation_1 is
      LM : Lock_Manager (Lock'Access) with Unreferenced;
   begin
      State := State + 1;  -- for example...
      Put_Line ("State is now" & State'Img);
   end Operation_1;

   procedure Operation_2 is
      LM : Lock_Manager (Lock'Access) with Unreferenced;
   begin
      State := State - 1;  -- for example...
      Put_Line ("State is now" & State'Img);
   end Operation_2;

end P;

The aspect Unreferenced tells the compiler that no references in the source code are expected. That has two effects during compilation. First, warnings about the lack of references in the source code are disabled. Ordinarily we'd want those warnings because an unreferenced object usually indicates a coding error. That warning would be noise for objects of this type. But by the same token, the compiler will issue a warning if some explicit reference is present, perhaps added much later in the project lifetime.

Pros

Race conditions are precluded, the client code is simpler than direct manual calls, and the code is robust, especially concerning exceptions. These advantages are significant, given the cost in engineering time to debug the errors this design prevents.

Cons

The lock is global, so all calls go through it. Hence all calls are sequential, even if some could run concurrently. In the above example that's exactly as required, but in other situations it might be unnecessarily limiting.

Compared to the manual call approach, the run-time cost for keeping track of objects to be finalized could be non-trivial. That's likely true in any language.

Relationship With Other Idioms

None.

Notes

  • The name for a similar type in the C++ Boost library is Scoped_Lock, as is the Ada type in the GNAT library package GNATColl.Locks. I used Scope_Lock in AdaCore's Gem #70 [4].

  • I didn't invent the name Scope_Lock or the Ada implementation, but I don't recall where I first saw it many years ago. My apologies to that author.

  • I consider the name Lock_Manager or something similar to be better, since objects of the type are wrappers for locks, not locks themselves. Indeed, in C++ 2011 the name is lock_guard.

Bibliography