user5069935
user5069935

Reputation:

Ada - How to implement an asynchronous task that allows the main thread to poll it?

I want to create a task that reads from a file for a few minutes while the main thread does other things. But I'd like the main thread to be able to poll the task to see if it is "Busy" or not (a Boolean value) without blocking the main thread.

I have a naive attempt here, which does work but it leaves the Busy flag completely exposed to be toggled at will by the main thread (this is not safe)...

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is

  task type Non_Blocking_Reader_Task (Busy : access Boolean) is
    entry Read (Destination : in Natural);
  end Non_Blocking_Reader_Task;

  task body Non_Blocking_Reader_Task is
  begin
    loop
      select
        when not Busy.all =>
          accept Read (Destination : in Natural) do
            Busy.all := True;
          end Read;

          for i in 1 .. 50 loop
            Put ("."); --  pretend to do something useful
            delay 0.1; --  while wasting time
          end loop;

          Busy.all := False;
      end select;
    end loop;
  end Non_Blocking_Reader_Task;

  Reader_Busy_Volatile : aliased Boolean;
  Reader : Non_Blocking_Reader_Task (Reader_Busy_Volatile'Access);
begin

  Put_Line (Reader_Busy_Volatile'Image);

  Reader.Read (123);

  for i in 1 .. 15 loop
    Put_Line (Reader_Busy_Volatile'Image);
    delay 0.5;
  end loop;

  abort Reader;
end Main;

My second idea was to create a protected type and hide the flag and the task inside it, but this is not permitted by the language.

Question

How can I create a protected "task is busy" flag that can is read-only from the main thread and read/write from the task (which does not cause the main thread to block)?


Edit:

The solution!

My revised (working) solution based on the stirling advice of @flyx :)

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is
   task type Reader_Task is
      entry Read (Destination : in Natural);
      entry Join;
      entry Ready;
   end Reader_Task;

   task body Reader_Task is
      Dest : Natural;
   begin
      loop
         select
            accept Read (Destination : in Natural) do
               Dest := Destination;
            end Read;

            --  we only get here after a Read has been received.
            for i in 1 .. 5 loop
               Put ("."); --  pretend to do something useful
               delay 1.0; --  while wasting time
            end loop;
         or
            accept Join;
         or
            accept Ready;
         or
            terminate;
         end select;
      end loop;
   end Reader_Task;

   Reader : Reader_Task;
begin
   --  first call will not block.
   Reader.Read (123);
   Put_Line ("MAIN: Reading in progress on second thread");

   for i in 1 .. 12 loop
      select
         --  NON-BLOCKING CALL!
         Reader.Ready; -- test if task is busy
         Put_Line ("MAIN: NON-BLOCKING CALL          SUCCEEDED -- TASK IS NOT BUSY");
      else
         Put_Line ("MAIN: NON-BLOCKING CALL FAILED             -- TASK IS BUSY");
      end select;

      delay 1.0;
   end loop;

   Put_Line ("Main: Waiting for Reader (BLOCKING CALL)...");
   Reader.Join;
   Put_Line ("Main: all finished!");
end Main;

I've added two more entries to the task: Join and Ready which are basically the same but for the names. Join reminds me to do a blocking call to it, and Ready indicates that a non-blocking call is suitable for testing task availability. I've done this because there are times when I want to know if the previous run of Read() has finished without firing off a new one. This lets me do this neatly and all without any discrete flags at all! Awesome.

Upvotes: 1

Views: 1306

Answers (1)

flyx
flyx

Reputation: 39708

In Ada, the caller decides whether an entry call is blocking or not. You should not try and implement code for checking this inside the task.

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is
   task type Reader_Task is
      entry Read (Destination : in Natural);
   end Reader_Task;

   task body Reader_Task is
   begin
      loop
         select
            accept Read (Destination : in Natural) do
               null;
               -- store Destination (?)
            end Read;
         or
            -- allow task to be terminated while waiting
            terminate;
         end select;

         --  we only get here after a Read has been received.
         for i in 1 .. 50 loop
            Put ("."); --  pretend to do something useful
            delay 0.1; --  while wasting time
         end loop;
      end loop;
   end Reader_Task;

   Reader : Reader_Task;
begin
   --  first call will not block.
   Reader.Read (123);

   for i in 1 .. 15 loop
      --  check whether Read can be called immediately and if yes,
      --  call it.
      select
         Reader.Read (456);
      else
         -- Read is not available, do something else.
         null;
      end select;

      delay 0.5;
   end loop;

   -- don't call abort; let Reader finish its current task.
   -- Reader will be terminated once it waits on the terminate alternative
   -- since the parent is finished.
end Main;

The select … else … end select structure is Ada's way of doing a non-blocking call. You don't use a flag for signalling that the task is ready to receive an entry call because potentially, this state could change between the query of the flag and the actual call of the entry. select has been designed to avoid this problem.

Upvotes: 4

Related Questions