File read/write advisory locking in Ada?

Hi;

I may have missed this in the docs, forgive me please if so…

If I have two or more processes (tasks perhaps?) that are reading and/or writing to a file, how can I obtain temporary locking?

I certainly do not want file corruption.

I really do not want to have an unhandled exception from being uable to get access to the file by the second process while the first process does have access, which would result in the program exiting.

Perhaps I have to do exception handling, have a delay, and try again, if the second process can’t get access?

Perhaps “good” Ada programmers in a program that does multiprocessing only has one task that does the I/O for a specific file and it performs that locking and the rendezvous synchronization so that all the other processes have to busy wait for the task responsible for I/O to accept requests for the file?

Thanks,
Retired Build Engineer

Re-reading the docs…I should just use the “Is_Open” function provided by Ada.Text_IO, and then use a busy-wait loop if necessary until available?

I guess I was over-thinking it :frowning:

Thanks,
Retired Build Engineer

1 Like

One method would be to have a task-type, something like:

-- Assuming with/use of Ada.Text_IO & Ada.Strings.Unbounded.
Task Type Write_Control( File : not null access File_Type ) is
   Entry Write( Input : String );
   Entry Done;
End Write_Control;

-- and, maybe:
Function Create( File : in out Ada.Text_IO.File_Type ) return Write_Control is
( Write_Control( File => New Ada.Text_IO.File_Type'(File) ) );

-- …
Task Body Write_Control is
  Package US renames Ada.Strings.Unbounded;
  Working : US.Unbounded_String;
  Finished: Boolean:= False;
  Function "+"(Right : US.Unbounded_String) return String
    renames US.To_String;
  Function "+"(Right : String) return US.Unbounded_String
    renames US.To_Unbounded_String;
Begin
   Loop
      Exit when Finished;
      select
         Entry Write( Input : String ) do
            Working:= +Input;
         End Write;
         Ada.Text_IO.Put( Item => +Working, File => File.All );
      or
         Entry Done;
         Finished:= True;
      end select;
   End loop;
End Write_Control;

This forces access to the file through the task’s entry, which can only be accessed by a single task at a time, giving you the appropriate control… I typically use a helper-function that takes the file’s name, opens the file (creating it if it doesn’t exist) and returning that – an excellent case for using extended-return:

Function Create(Name : String) return File_Type is
Begin
  Return Result : File_Type do
    -- Attempt open, on exception
    -- attempt to create.
  End return;
End;

This construction means that I’ll get an exception, or the function-call returns the file, opened and ready to use…
(Note this is on-the-fly/off-the-cuff non-compiled code.)

1 Like

The “monitor” solution was already presented for single process. Here is a solution based on a mutex for the sake of completeness:

with Ada.Text_IO;  use Ada.Text_IO;

with Ada.Finalization;
with Ada.Task_Identification;
with Synchronization.Mutexes;

procedure Main is
   package Shared_Files is
      type Shared_File is tagged limited private;
      procedure Create (F : in out Shared_File; Name : String);
      procedure Put_Line (F : in out Shared_File; S : String);
   private
      use Synchronization.Mutexes;
      type Shared_File is
         new Ada.Finalization.Limited_Controlled with
      record
         File : File_Type;
         Lock : aliased Mutex;
      end record;
      procedure Finalize (F : in out Shared_File);
   end Shared_Files;

   package body Shared_Files is
      procedure Finalize (F : in out Shared_File) is
         Exclusive : Holder (F.Lock'Access);
      begin
         if Is_Open (F.File) then
            Close (F.File);
         end if;
      end Finalize;
      
      procedure Create (F : in out Shared_File; Name : String) is
         Exclusive : Holder (F.Lock'Access);
      begin
         Create (F.File, Out_File, Name);
      end Create;

      procedure Put_Line (F : in out Shared_File; S : String) is
         Exclusive : Holder (F.Lock'Access);
      begin
         Put_Line (F.File, S);
      end Put_Line;

   end Shared_Files;

   use Shared_Files;

   File : Shared_File;

   task type Spammer;

   task body Spammer is
      use Ada.Task_Identification;
   begin
      for I in 1..100 loop
         File.Put_Line (Image (Current_Task) & ":" & Integer'Image (I));
      end loop;
      File.Put_Line (Image (Current_Task) & ": Bye");
   end Spammer;
begin
   File.Create ("test.txt");
   declare
      Talkers : array (1..10) of Spammer;
   begin
      null;
   end;
end main;

It starts 10 tasks writing a shared file concurrently.

Now, if we talk about files shared between processes. Here are the points:

  • Files must be opened for shared access.
  • You can use a system-wide mutex with the solution above or else an RPC for “monitor” solution.

In Simple components for Ada you can find both interprocess mutex and RPCs. But before you even start, consider it a bad idea. A system logging facility or a database would be most likely a far better idea.

1 Like

I rather like being able to abstract out the DB/logger into its own task, if possible.

If you do it right, you can essentially use “MVC” on the front-end and the back-end, keeping your program’s objects/data homogeneous and abstracting away any notion that (e.g.) the back-end is CRUD database actions.

It really is sad how many programmers are unaware of the technique because so many applications leak those details because (a) C/PHP is bad at abstracting it out; (b) a library has a strong notion of how data/objects are handled/processed, and it’s “easier” to just work on those terms; and/or (c) management bought the notion that “application development = writing glue-code between various libraries”.

1 Like