Prev: About the F-22 software bug
Next: Tue Feb 23 Seminar - Ada and SPARK for education and research
From: Hibou57 (Yannick Duchêne) on 5 Feb 2010 15:54 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 5 Feb 2010 15:56 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 5 Feb 2010 16:38 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 5 Feb 2010 16:40 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 5 Feb 2010 16:53 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...).
|
Next
|
Last
Pages: 1 2 3 4 5 6 7 Prev: About the F-22 software bug Next: Tue Feb 23 Seminar - Ada and SPARK for education and research |