From: Hibou57 (Yannick Duchêne) on
Hello Ada novel writers,

I'm starting to experiment with Ada tasking, which as I suggest, I've
never used so far (I will need at least it for a Windows API binding
and for a later VST binding... a VST is a kind of software synthesizer
for electronic music).

Let me explain the context before I introduce my though. I was playing
with a source from a tutorial.
This source : http://www.infres.enst.fr/~pautet/Ada95/e_c26_p2.ada
From this tutorial : http://www.infres.enst.fr/~pautet/Ada95/chap26.htm

Basically, three task running concurrently, display text to the
standard output stream. I've tried this one, and noticed that as the
output from each task was not atomic, output from all these tasks
ended to interlaced output on the console (notice the message each
task is displaying, made of three statement).

So I wanted to solve this, and added a protected object providing an
Put procedure which was displaying the same message, all three
statement in an atomic procedure.

It was fine and output was no more interlaced.

Later, I wanted to play a bit more and do the same with a task instead
of a protected object.

It was a task with a single entry and accept statement, Put, doing the
same, and a Terminate statement triggered when there was no more
request queued.

Later, I advised the task may terminates while some potential clients
may still be alive, or even the task may terminates before any client
had time to make any first request.

So I've decided to add a Aquire and Release entry, whose purpose was
to increase and decrease a counter, telling the output task server
that some potential clients were still alive or not. As a special
case, the initial zero value of the counter was not construed as a
terminating condition, for the same reason as explained above (some
clients may not had time to make an initial request) and it was only
interpreted as such when it was going from a non-zero value to the
zero value.

Ok, it was working fine.

Later, I wanted to make it more secure and though it would be nice to
manage these Acquire/Release invocations transparently, using a
Limited_Controller type. All clients were then required to own an
instance of this type and to pass it to the Put entry, as a proof the
client was indeed owning one. This type was named Ticket_Type.

I've created a separate package for the output task and its associated
Ticket_Type, Well, I wanted the Acquire/Release entry of the output
task to not be public any more and the Ticket_Type to be private.
Trouble : there was no way to declare the task in the public part with
a public Put entry and to give it Acquire/Release entries which would
be visible only in the private part and implementation.

The output server task only had a public Put entry, and the Acquire/
Release method were moved to a protected counter object in the
implementation. The Ticket_Type were invoking Acquire/Release on this
protected object, and the output task were requesting the counter
status on this same protected counter object.

All this story to tell to finally understand that making the output
task public, ended into more complex tricks that what I would have get
with a simple publicly declared Put procedure, implemented on top of a
task in the implementation only (the task would have had an Acquire
and Release entry, which the Ticket_Type implementation would have
accessed directly, as it would have been made private in the
implementation).

Here is my though (talking about design principle) : are tasks mostly
to be treated as things to be exposed in public part of to be treated
as implementation details ? After this experiment, I'm filling like
task are just like record components, that is, better to make these
private.

Any way, a request made to a task is blocking as long as the task is
not ready to fulfill the request (the rendezvous, which is the Ada
primitive for synchronization). So, from the client point of view, a
method implemented on top of a task is not a different thing than a
method implemented on top of a procedure.

Any experienced comments about it ?

For inquisitive peoples, here is the source (see next post) designed
with a public task, which make me think a private task is perhaps in
most of case better than a public task.

Notice there was another trouble with this source : when some
accessory stuff were removed, the output task was blocking forever,
never triggering the Terminate statement. I was circumspect about it
until I've understood this was because the select statement was not
evaluated as long as no more queued request were pending. I've solved
this in another version (not this one), using a kind of pooling with
delay..... but I do not like pooling, so I'm still working on the
subject.
From: Hibou57 (Yannick Duchêne) on
The source I'm referring to in the previous post :

with Ada.Text_IO;
-- For the service provided by the
-- Output_Server package.
with Ada.Finalization;
-- For the Ticket_Type implementation.

procedure Test is

package Text_IO renames Ada.Text_IO;

package Output_Server is
-- Provides a server which outputs text on
-- the standard output stream, in an atomic
-- maner. Ths purpose of this package is
-- to experiment and the better implementation
-- for such a requirement, would have been to
-- simply use a protected type. This is an
-- experiment to do the same with a task
-- instead.

type Ticket_Type is limited private;
-- Any request to Instance.Put must be
-- made providing a ticket. The Instance task
-- will live at least as long as a ticket
-- is alive in scope and as long as there is
-- no more ticket alive.

task Instance is
entry Put
(Ticket : in Ticket_Type;
Text : in String;
Number : in Natural);
-- Print Text follow by the image of Number
-- on a new line on the standard output. Keep
-- in mind this is just an experiment example.
--
-- The normal way to complete a task in Ada
-- (not to be confused with task termination),
-- is to either have executed all of its task
-- body [ARM 9.3(5)] or to be blocked on select
-- statement with an opened terminate alternative
-- [ARM 9.3(6/1)], that is, if there is no other
-- opened accept statement matching a queued
-- request [ARM 9.7.1(16)] (providing the task
-- body is built around a Select Accept block).
--
-- The "trouble" with this, is that this will
-- be completed as soon there will be no more
-- queued request pending. Indeed, the task
-- semantic refer to actual state and in some
-- way to the previous state, but cannot refer
-- to any futur or potentially futur state.
--
-- We want to complete the task, only when we
-- will know no futur request could come.
-- A specification may be to tell we are in
-- such a case when no more client task are
-- alive (when they are either all completed
-- or terminated).
--
-- So we need a way to know which tasks are
-- clients of this server task. This is the
-- purpose of the ticket of Ticket_Type which
-- comes as a required parameter of the Put
-- entry.
end Instance;

private

-- There use to be an implementation where
-- the server task exposed an Acquire and
-- Release entry, which client were to invok
-- to declare they are client and later no
-- more client of the server task. To
-- be more secure, it was later decided to
-- manage it automatically via the
-- Initialize/Finalize capabilities of a
-- Limited_Controlled type.
--
-- Clients do not invok any more Acquire
-- and then later Release, they just have
-- to own a ticket, which automatically
-- do the stuff as its initialization
-- and later finalization part. This
-- is more safe and less error prone
-- on the client side.

type Ticket_Type is
limited
new Ada.Finalization.Limited_Controlled
with null record;

overriding procedure Initialize
(Ticket : in out Ticket_Type);
-- Initialization of a ticket register
-- a client.

overriding procedure Finalize
(Ticket : in out Ticket_Type);
-- Finalization of a ticket unregister
-- a client.

end Output_Server;

package body Output_Server is

-- Ticket_Type is a private type of the
-- Output_Server package. It needs to
-- inform the Instance task (the server
-- task) when a ticket is entering into
-- existance and when it is later no
-- more alive.
--
-- Unfortunately, we cannot provide Acquire
-- and Release entries which will be only
-- accessible to the implementation of
-- Ticket_Type.
--
-- So, we need another way the Ticket_Type
-- and the Instance to be able to communicate
-- privately. This is done via the following
-- Clients_Counter protected object.

protected Clients_Counter is
procedure Acquire;
procedure Release;
function Count return Natural;
function Has_Started return Boolean;
private
-- These two members use the postffix _Value
-- to avoid a name clash with the two
-- corresponding functions.
Count_Value : Natural := 0;
Has_Started_Value : Boolean := False;
-- The number of registered clients is first
-- zero. The Instance task completes as soon
-- as there is no queued request and the number
-- of living registered clients is zero.
--
-- But the start time is a special case : we
-- are waiting for client to register. So
-- we will not consider the first zero value,
-- and will only take care of a zero value
-- when it will go down to zero.
--
-- This is the purpose of Has_Started_Value.
--
-- Thus, the Instance task can request to
-- both Count and Has_Started status.
end Clients_Counter;

protected body Clients_Counter is

procedure Acquire is
begin
Has_Started_Value := True;
Count_Value := Count_Value + 1;
Text_IO.Put_Line
("Acquire: Count is now " &
Natural'Image (Count_Value) &
"and Has_Started is now " &
Boolean'Image (Has_Started_Value));
-- Log Clients_Counter activities for
-- debuging purpose.
end Acquire;

procedure Release is
begin
Count_Value := Count_Value - 1;
Text_IO.Put_Line
("Release: Count is now " &
Natural'Image (Count_Value));
-- Log Clients_Counter activities for
-- debuging purpose.
end Release;

function Has_Started return Boolean is
begin
-- {Location #1}: if this log
-- statement is removed, then
-- the Instance task blocked
-- indefinitely at location #2.
Text_IO.Put_Line
("Has_Started: returned " &
Boolean'Image (Has_Started_Value));
-- Log Clients_Counter activities for
-- debuging purpose.
return Has_Started_Value;
end Has_Started;

function Count return Natural is
begin
Text_IO.Put_Line
("Count: returned " &
Natural'Image (Count_Value));
-- Log Clients_Counter activities for
-- debuging purpose.
return Count_Value;
end Count;

end Clients_Counter;

overriding procedure Initialize
(Ticket : in out Ticket_Type)
-- Initialization of a ticket register
-- a client.
is
begin
Clients_Counter.Acquire;
end Initialize;

overriding procedure Finalize
(Ticket : in out Ticket_Type)
-- Finalization of a ticket unregister
-- a client.
is
begin
Clients_Counter.Release;
end Finalize;

task body Instance
is
begin
while True loop
select
accept Put
(Ticket : in Ticket_Type;
Text : in String;
Number : in Natural)
do
pragma Unreferenced (Ticket);
-- The sole use of the ticket is
-- to require the client to actually
-- own a ticket.
Text_IO.Put_Line
(Text &
" " &
Natural'Image (Number));
end Put;
or
-- {Location #2}: this work find
-- as long a there is a log statement
-- at location #1. If this statement
-- is removed, then the task never
-- reach its terminate alternative.
when
(Clients_Counter.Has_Started) and
(Clients_Counter.Count = 0)
=>
terminate;
end select;
end loop;
end Instance;

end Output_Server;

-- Shortcuts for better readability.

subtype Ticket_Type is
Output_Server.Ticket_Type;

procedure Put
(Ticket : in Ticket_Type;
Text : in String;
Number : in Natural)
renames
Output_Server.Instance.Put;
-- Keep in mind it's a task entry.

-- Now comes three simple task.
-- Al have the same layout. The
-- first one is the sole commented
-- one.

task First_Task;
task body First_Task
is
Ticket : Ticket_Type;
-- Automatically register this
-- task as client when we enter the
-- scope and unregister this task
-- when the task is terminated.
begin
for Index in 1..4 loop
Put
(Ticket,
"This is First_Task, passing number ",
Index);
-- Remember Put is a request to the
-- Output_Server.Instance.Put entry.
end loop;
end First_Task;

-- Second task : same thing.

task Second_Task;
task body Second_Task
is
Ticket : Ticket_Type;
begin
for Index in 1..7 loop
Put
(Ticket,
"This is Second_Task, passing number",
Index);
end loop;
end Second_Task;

-- Third task : same story.

task Third_Task;
task body Third_Task
is
Ticket : Ticket_Type;
begin
for Index in 1..5 loop
Put
(Ticket,
"This is Third_Task, passing number ",
Index);
end loop;
end Third_Task;

begin
null;
-- Nothing there, all is done in tasks.
-- The application terminates when the last
-- task terminates.
--
-- The tasks which will be started by the environment
-- task are : Output_Server.Instance, First_Task,
-- Second_Task and Third_Task.
end Test;
From: Jeffrey R. Carter on
Hibou57 (Yannick Duch�ne) wrote:
>
> So I wanted to solve this, and added a protected object providing an
> Put procedure which was displaying the same message, all three
> statement in an atomic procedure.

Technically this is a bounded error: Ada.Text_IO.Put* operations are potentially
blocking, and should not be called from a protected operation.

> Later, I advised the task may terminates while some potential clients
> may still be alive, or even the task may terminates before any client
> had time to make any first request.

This should not happen. Did you actually experience this?

--
Jeff Carter
"He didn't get that nose from playing ping-pong."
Never Give a Sucker an Even Break
110
From: Dmitry A. Kazakov on
On Fri, 5 Feb 2010 12:54:42 -0800 (PST), Hibou57 (Yannick Duch�ne) wrote:

> So I wanted to solve this, and added a protected object providing an
> Put procedure which was displaying the same message, all three
> statement in an atomic procedure.

This is illegal, because protected procedure shall not perform potentially
blocking actions (like I/O.

> Later, I advised the task may terminates while some potential clients
> may still be alive, or even the task may terminates before any client
> had time to make any first request.

You do not need to worry about that. Unless you are using pointers, the
task object's scope encloses any calls to its entries. Therefore it simply
cannot terminate due to its finalization before any entry call. If it
terminates for any other reason, you get Tasking_Error in the entry calls.

> Later, I wanted to make it more secure and though it would be nice to
> manage these Acquire/Release invocations transparently, using a
> Limited_Controller type. All clients were then required to own an
> instance of this type and to pass it to the Put entry, as a proof the
> client was indeed owning one. This type was named Ticket_Type.

If you want garbage collection because of pointers involved then just do
it. Don't break the task interface, make a handle type pointing to the task
object. When the last handle vanishes, deallocate the task. That is.

> Here is my though (talking about design principle) : are tasks mostly
> to be treated as things to be exposed in public part of to be treated
> as implementation details ? After this experiment, I'm filling like
> task are just like record components,

Task component does not work in most real-life cases.

> that is, better to make these private.
> Any way, a request made to a task is blocking as long as the task is
> not ready to fulfill the request (the rendezvous, which is the Ada
> primitive for synchronization). So, from the client point of view, a
> method implemented on top of a task is not a different thing than a
> method implemented on top of a procedure.

Yes, an entry call can be syntactically and semantically different than a
call to a procedure.

Here is how I would do this:

with Ada.Text_IO;

procedure Test is

task Monitor is -- "Monitor" is the customary name of this pattern
entry Put (Text : in String; Number : in Natural);
end Monitor;

task body Monitor is
begin
loop
select
accept Put (Text : in String; Number : in Natural) do
Ada.Text_IO.Put_Line (Text & " " & Natural'Image (Number));
end Put;
or terminate;
end select;
end loop;
exception
when others =>
Text_IO.Put_Line ("Oops, there is something bad here");
end Monitor;

task First_Task;
task body First_Task is
begin
for Index in 1..4 loop
Monitor.Put ("This is First_Task, passing number ", Index);
end loop;
end First_Task;

task Second_Task;
task body Second_Task is
begin
for Index in 1..7 loop
Monitor.Put ("This is Second_Task, passing number", Index);
end loop;
end Second_Task;

task Third_Task;
task body Third_Task is
begin
for Index in 1..5 loop
Monitor.Put ("This is Third_Task, passing number ", Index);
end loop;
end Third_Task;

begin
null;
end Test;

You can print from the rendezvous. That is not a problem. Some tight
implementations would prefer buffering in the rendezvous in order to
release the caller as soon as possible (minimizing the effect of priority
inversion). I.e. Text is first copied into the task's buffer during the
rendezvous and printed later outside the rendezvous before accepting the
new one. Assuming that the callers run at a higher priority and do not
print very often leaving the processor free most of the time, this would
give better response times in the callers (at the cost of some overall
performance hit).

--
Regards,
Dmitry A. Kazakov
http://www.dmitry-kazakov.de
From: Hibou57 (Yannick Duchêne) on
Hi Jeffrey, nice to meet you again,

On 5 fév, 22:38, "Jeffrey R. Carter" <spam.jrcarter....(a)spam.acm.org>
wrote:
> Technically this is a bounded error: Ada.Text_IO.Put* operations are potentially
> blocking, and should not be called from a protected operation.
I did not ever suspected such a requirement. Transitive blocking is
not allowed ?

So, if blocking operation are not allowed from a protected type,
clients of a given operation have to know it weither or not it's
potentially blocking, and so, this fact must be stated in public part
of specifications, so then, the protected and tasked aspect of a
method must be stated in specifications and I suppose it's not a good
idea to make it private.

Wrong or right assumptions ?

> This should not happen. Did you actually experience this?
No, I did not experience it, this was just my imagination : I knew a
task may completes at its own discretion. Thus if may possibly
completes too much soon if its completion condition is not well
designed.

I will have to check the RM, but I'm pretty sure a completed task
cannot handle any request any more (at least, this seems to be a
reasonable assumption to me, but I will still have to check...).