Dealing with Silent Task Termination¶
Motivation¶
A task completes abnormally when an exception is raised in its sequence of statements and is not handled. Even if the task body has a matching exception handler and it executes, the task still completes after the handler executes, although this time it completes normally. Similarly, if a task is aborted the task completes, again abnormally.
Whatever the cause, once completed a task will (eventually) terminate, and it does this silently — there is no notification or logging of the termination to the external environment. A vendor could support notification via their run-time library [1], but the language standard does not require it and most vendors — if not all — do not.
Nevertheless, applications may require some sort of notification of the event that caused the termination. Assuming the developer is responsible for implementing it, how can the requirement best be met?
Implementation¶
For unhandled exceptions, the simplest approach to silent termination is to define the announcement or logging response as an exception handler located in the task body exception handler part:
with Ada.Exceptions; use Ada.Exceptions;
with Ada.Text_IO; use Ada.Text_IO;
-- ...
task body Worker is
begin
-- ...
exception
when Error : others => -- last wishes
Put_Line ("Task Worker terminated due to " & Exception_Name (Error));
end Worker;
A handler at this level expresses the task's last wishes prior to completion,
in this case printing the names of the task and the active exception to
Standard_Output. (We could print the associated exception message too, if desired.)
The others choice covers all exceptions not
previously covered, so in the above it covers all exceptions. Specific
exceptions also could be covered, but the others choice should be
included (at the end) to ensure no exception occurrence can be missed.
You'll probably want this sort of handler for every application task if you want it for any of them. That's somewhat inconvenient if there are many tasks in the application, but feasible. Possible mitigation includes the use of a task type. In that case you need only define the handler once, in the task type's body. You could even declare such a task type inside a generic package, with generic formal subprograms for the normal processing and the exception handler's processing. That would make the task type reusable. But that's a bit heavy, and it can be awkward. See the Notes section below for details.
Alternatively, we could prevent unhandled exceptions from causing termination in the first place. We can do that by preventing task completion, via some additional constructs that prevent reaching the end of the task's sequence of statements. We will show these constructs incrementally for the sake of clarity.
Before we do, note that many tasks are intended to run until power is removed,
so they have an infinite loop at the outer level as illustrated in the code
below. For the sake of clarity and realism, we name the loop Normal and
call some procedures to show the typical structure, along with the last wishes
handler:
task body Worker is
begin
Initialize_State;
Normal : loop
Do_Actual_Work;
end loop Normal;
exception
when Error : others => -- last wishes
Put_Line ("Task Worker terminated due to " &
Exception_Name (Error));
end Worker;
In the above, the procedures' names indicate what is done at that point in the code. The steps performed may or may not be done by actual procedure calls.
Strictly speaking, the optional exception handler part of the task body is the very end of the task's sequence of statements (the handled sequence of statements). We want to prevent the thread of control reaching that end — which would happen if any handler there ever executed — because the task would then complete.
Therefore, we first wrap the existing code inside a block statement. The task body's exception handler section becomes part of the block statement rather than at the top level of the task:
task body Worker is
begin
begin
Initialize_State;
Normal : loop
Do_Actual_Work;
end loop Normal;
exception
when Error : others => -- last wishes
Put_Line ("Task Worker terminated due to " &
Exception_Name (Error));
end;
end Worker;
Now any exception raised within Initialize_State and
Do_Actual_Work will be caught in the block statement's handler, not the
final part of the task's sequence of statements. Nothing else changes,
semantically. The task will still complete because the block statement exits
after the handler executes, and so far there's nothing after that block
statement. We need to make one more addition.
The second (and final) addition prevents reaching the end of the sequence of
statements after a handler executes, and hence the task from completing. This
is accomplished by wrapping the new block statement inside a new loop
statement. We name this outermost loop Recovery:
task body Worker is
begin
Recovery : loop
begin
Initialize_State;
Normal : loop
Do_Actual_Work;
end loop Normal;
exception
when Error : others =>
Put_Line (Exception_Name (Error) & " handled in task Worker");
end;
end loop Recovery;
end Worker;
When the block exits after the exception handler prints the announcement,
normal execution resumes and the end of the Recovery loop is reached.
The thread of control then continues at the top of the loop. Of course, absent
an unhandled exception reaching this level, the Normal loop is never
exited in the first place.
These two additions ensure that — with one caveat — Worker
never terminates due to an unhandled exception raised during execution of the
task's sequence of statements.
Note that an exception raised during elaboration of the task body's declarative part is not handled by the approach, or any other approach at this level, because the exception is propagated immediately to the master of the task. Such a task never reaches the handled sequence of statements in the first place.
The caveat concerns the language-defined exception Storage_Error. This
exception requires special consideration because the reasons for raising it
include exhausting the storage required for execution itself.
There are a couple of scenarios to consider.
The first scenario is task activation, i.e., creation. Initial task activation
involves execution (in the tasking part of the run-time library) before the
sequence of steps is reached. Hence task activation for the Worker task
could fail due to an insufficient initial storage allocation. But because that
failure happens before the block statement is entered it doesn't really apply
to the caveat above.
The second scenario involves execution within the task's actual sequence of statements. Therefore it does apply to the caveat above. Here's why.
When called, the execution of a given subprogram requires a representation in storage, often known as a frame. Because subprogram calls and their returns can be seen as a series of stack pushes and pops, the representation for execution is typically via a stack of these frames. Calls cause stack frame pushes, creating new frames on the stack, and returns cause stack pops, reclaiming the frames. On exit, execution returns to the caller, so the previous top of the stack is now the active frame. Representation as a stack of frames works well so it is very common. (Functions returning values of unconstrained types are problematic because the size of the result isn't known at the point of the call, so the required frame size isn't known when the push occurs. Solutions vary, but that's a topic for another day.)
Now, suppose the task's sequence of statements includes a long series of subprogram calls, in which one subprogram calls another, and that one calls another, and so on, and none of these calls has yet returned. Eventually, of course, the dynamic call chain will end because the calls will return, at least in normal code. But let's suppose that the call chain is long and the most recent call has not yet returned to the caller.
In that case it is possible for one more call to exhaust the storage available for that task's execution. You can easily construct such a chain by calling an infinitely recursive procedure or function:
procedure P;
procedure P is
begin
P;
end P;
When executing on a host OS it might take a very long time for a call to
P to exhaust available storage, maybe longer than you'd be willing to
wait. But on an embedded system, where physical storage is limited and there's
no virtual memory, it might not take long at all.
Now, you might think that you don't use recursion, much less infinitely recursive routines, so this problem doesn't apply to you. But recursion is just an easy illustration. How long a call chain is too long? It depends on the memory resources available.
Moreover, exhaustion is not due only to the storage required for the call/return semantics. Frames include the representation of the local objects declared within the subprograms' declarative parts, if any.
procedure P;
procedure P is
Local : Integer;
begin
Local := 0;
P;
end P;
Each execution of a call to P creates a semantically distinct instance
of Local. A new frame containing the storage for each call's copy of
Local implements that requirement nicely.
Of course, different subprograms usually declare different local objects, if they declare any at all. Because the storage required for these declarations varies, the corresponding frame sizes vary.
We can use that fact to reduce the length of the dynamic call chain required to illustrate storage exhaustion. The called subprograms will declare very large objects within their declarative parts. Hence each frame is correspondingly larger than if the subprogram declared nothing locally. Continuing the infinitely recursive subprogram example:
procedure P;
procedure P is
type Huge_Component is array (Long_Long_Integer) of Long_Float;
type Huge_Array is array (Long_Long_Integer) of Huge_Component;
Local : Huge_Array;
begin
Local := (others => (others => 0.0));
P;
end P;
The size of the frame for an individual call to P will be very large
indeed, if it is representable at all. Fewer calls will be required before
Storage_Error is raised.
Now, with all that said, let's get back to this approach to silent termination. Here's the code again:
task body Worker is
begin
Recovery : loop
begin
Initialize_State;
Normal : loop
Do_Actual_Work;
end loop Normal;
exception
when Error : others =>
Put_Line (Exception_Name (Error) & " handled in task Worker.");
end;
end loop Recovery;
end Worker;
At this point you might be thinking that Storage_Error would be caught
by the others choice anyway, so this (long-winded) talk about stack
frames and dynamic call chains is irrelevant. That's where the caveat comes
into play.
Specifically, if there's insufficient storage remaining for execution to continue, how how do we know there's enough storage remaining to execute the exception handler? For that matter, how do we even know there's enough storage available for the run-time library to find the handler in the first place? Absent a storage analysis, we can't know with certainty.
Therefore, if the application matters, perform a worst-case storage analysis per task, including the exception handlers, and explicitly specify the tasks' stacks accordingly. For example:
task Worker with Storage_Size => System_Config.Worker_Storage;
We've defined the value as a constant named Worker_Storage declared in
an application-defined package System_Config. All such values are
declared in that package, for the sake of centralizing all the application's
configuration parameters. We'd declare all the tasks' priorities there too.
Finally, although this approach works, the state initialization requires some thought.
As shown above, full initialization is performed again when the
Recovery loop circles back around to the top of the loop. As a result,
the normal processing in Do_Actual_Work must be prepared for suddenly
encountering completely different state, i.e., a restart to the initial state.
If that is not feasible the call to Initialize_State could be moved
outside, prior to the start of the Recovery loop, so that it only
executes once. Perhaps a different initialization procedure could be called
after the exception handler to do partial initialization. Whether or not that
will suffice depends on the application.
However, these approaches do not address task termination due to task abort statements.
Aborting tasks is both messy and expensive at run-time. If a task is updating some object and is aborted before it finishes the update, that object is potentially corrupted. That's the messiness. If an aborted task has dependent tasks, all the dependents are aborted too, transitively. A task in a rendezvous with the aborted task is affected, as are those queued waiting to rendezvous with it, and so on. That's part of the expensiveness when aborts are actually used. Worse, even if never used, abort statements impose an expense at run-time. The language semantics requires checks for an aborted task at certain places within the run-time library. Those checks are executed even if no task abort statement is ever used in the application. To avoid that distributed cost, you would need to apply a tasking profile disallowing abort statements and build the executable with a correspondingly reduced run-time library implementation.
As a consequence, aborting a task should be very rarely done. Regardless, the task abort statement exists. How can we express a last wishes response for that cause too?
Fortunately, Ada provides a facility that addresses all possible causes: normal termination, termination due to task abort, and termination due to unhandled exceptions.
With this facility developers specify procedures that are invoked automatically by the run-time library during task finalization. These procedures express the last wishes for the task, but do not require any source code within the task, unlike the exception handler in each task body described earlier. These response procedures are known as handlers.
During execution, handlers can be applied to an individual task or to groups of related tasks. Handlers can also be removed from those tasks or replaced with other handlers. Because procedures are not first-class entities in Ada, handlers are assigned and removed by passing access values designating them.
The facility is defined by package Ada.Task_Termination. The package
declaration for this language-defined facility follows, with slight changes for
easier comprehension.
with Ada.Task_Identification; use Ada.Task_Identification;
with Ada.Exceptions; use Ada.Exceptions;
package Ada.Task_Termination
-- ...
is
type Cause_Of_Termination is (Normal, Abnormal, Unhandled_Exception);
type Termination_Handler is access protected procedure
(Cause : in Cause_Of_Termination;
T : in Task_Id;
X : in Exception_Occurrence);
procedure Set_Dependents_Fallback_Handler
(Handler : in Termination_Handler);
function Current_Task_Fallback_Handler
return Termination_Handler;
procedure Set_Specific_Handler
(T : in Task_Id;
Handler : in Termination_Handler);
function Specific_Handler (T : Task_Id) return Termination_Handler;
end Ada.Task_Termination;
As shown, termination handlers are actually protected procedures, with a
specific parameter profile. Therefore, the type Termination_Handler is
an access-to-protected-procedure with that signature. The compiler ensures that
any designated protected procedure matches the parameter profile.
Termination handlers apply either to a specific task or to a group of related tasks, including potentially all tasks in the partition. Each task has one, both, or neither kind of handler. By default none apply. (Unless a partition is part of a distributed program, a single partition constitutes an entire Ada program.)
Clients call procedure Set_Specific_Handler to apply the protected
procedure designated by Handler to the task with the specific
Task_Id value T. These are known as specific handlers. The use
of a Task_Id to specify the task, rather than the task name, means that
we can set or remove a handler without having direct visibility to the task in
question.
Clients call procedure Set_Dependents_Fallback_Handler to apply the
protected procedure designated by Handler to the task making the call,
i.e., the current task, and to all tasks that are dependents of that task.
These handlers are known as fall-back handlers.
Handlers are invoked automatically, with the following semantics:
If a specific handler is set for the terminating task, it is called and then the response finishes.
If no specific handler is set for the terminating task, the run-time library searches for a fall-back handler. The search is recursive, up the hierarchy of task masters, including, ultimately, the environment task. If no fall-back handler is found no handler calls are made whatsoever. If a fall-back handler is found it is called and then the response finishes; no further searching or handler calls occur.
As a result, at most one handler is called in response to any given task termination.
The following client package illustrates the approach. Package
Obituary declares protected object Obituary.Writer, which
declares two protected procedures. Both match the profile specified by type
Termination_Handler. One such procedure would suffice, we just provide
two for the sake of illustrating the flexibility of the dynamic approach.
with Ada.Exceptions; use Ada.Exceptions;
with Ada.Task_Termination; use Ada.Task_Termination;
with Ada.Task_Identification; use Ada.Task_Identification;
package Obituary is
protected Writer is
procedure Note_Passing
(Cause : Cause_Of_Termination;
Departed : Task_Id;
Event : Exception_Occurrence);
-- Written by someone who's read too much English lit
procedure Dissemble
(Cause : Cause_Of_Termination;
Departed : Task_Id;
Event : Exception_Occurrence);
-- Written by someone who may know more than they're saying
end Writer;
end Obituary;
Clients can choose among these protected procedures to set a handler for one or more tasks.
The two protected procedures display messages corresponding to the cause of the termination. One procedure prints respectful messages, in the style of someone who's read too much Old English literature. The other prints rather dissembling messages, as if written by someone who knows more than they are willing to say. The point of the difference is that more than one handler can be available to clients, and their choice is made dynamically at run-time.
The package body is structured as follows:
with Ada.Text_IO; use Ada.Text_IO;
package body Obituary is
protected body Writer is
procedure Note_Passing () is ...
procedure Dissemble () is ...
end Writer;
begin -- optional package executable part
Set_Dependents_Fallback_Handler (Writer.Note_Passing'Access);
end Obituary;
In addition to defining the bodies of the protected procedures, the package
body has an executable part. That part is optional, but in this case it is
convenient. This executable part calls procedure
Set_Dependents_Fallback_Handler to apply one of the two handlers.
Because this call happens during library unit elaboration, it sets the
fall-back handler for all the tasks in the partition (the program). The effect
is global to the partition because library unit elaboration is invoked by the
environment task, and the environment task is the ultimate master of all
application tasks in a partition. Therefore, the fall-back handler is applied
to the top of the task dependents hierarchy, and thus to all tasks. The
application tasks need not do anything in their source code for the handler to
apply to them.
The call to Set_Dependents_Fallback_Handler need not occur in this
particular package body, or even in a package body at all. But because we want
it to apply to all tasks in this specific example, including library tasks,
placement in a library package's elaboration achieves that effect.
The observant reader will note the with-clause for Ada.Text_IO,
included for the sake of references to Put_Line. We'll address the
ramifications momentarily. Here are the bodies for the two handlers:
with Ada.Text_IO; use Ada.Text_IO;
package body Obituary is
protected body Writer is
procedure Note_Passing
(Cause : Cause_Of_Termination;
Departed : Task_Id;
Event : Exception_Occurrence)
is
begin
case Cause is
when Normal =>
Put_Line (Image (Departed) &
" went gently into that good night");
when Abnormal =>
Put_Line (Image (Departed) & " was most fouly murdered!");
when Unhandled_Exception =>
Put_Line (Image (Departed) &
" was unknit by the much unexpected " &
Exception_Name (Event));
end case;
end Note_Passing;
procedure Dissemble
(Cause : Cause_Of_Termination;
Departed : Task_Id;
Event : Exception_Occurrence)
is
begin
case Cause is
when Normal =>
Put_Line (Image (Departed) & " died, naturally.");
Put_Line ("We had nothing to do with it.");
when Abnormal =>
Put_Line (Image (Departed) & " had a tragic accident.");
Put_Line ("We're sorry it had to come to that.");
when Unhandled_Exception =>
Put_Line (Image (Departed) &
" was apparently fatally allergic to " &
Exception_Name (Event));
end case;
end Dissemble;
end Writer;
begin -- optional package executable part
Set_Dependents_Fallback_Handler (Writer.Note_Passing'Access);
end Obituary;
Now, about those calls to Ada.Text_IO.Put_Line. Procedure
Put_Line is a potentially blocking operation. Consequently, a call
within a protected operation is a bounded error (see RM 9.5.1(8)) and the
resulting execution is not portable. For example, the Put_Line calls
will likely work as expected on a native OS. However, their execution may do
something else on other targets, including raising Program_Error if
detected. The GNAT bare-metal targets, for example, raise Program_Error.
For a portable approach, we move these two blocking calls to a new dedicated task and revise the protected object accordingly. That's portable because a task can make blocking calls.
First, we change Obituary.Writer to have a single protected procedure
and a new entry. The protected procedure will be used as a termination handler,
as before, but does not print the messages. Instead, when invoked by task
finalization, the handler enters the parameter values into an internal data
structure and then enables the entry barrier on the protected entry. The
dedicated task waits on that entry barrier and, when enabled, retrieves the
stored values describing a termination. The task can then call Put_Line
to print the announcement with those values.
Here's the updated Obituary package declaration:
with Ada.Exceptions; use Ada.Exceptions;
with Ada.Task_Termination; use Ada.Task_Termination;
with Ada.Task_Identification; use Ada.Task_Identification;
with Ada.Containers.Vectors;
package Obituary is
pragma Elaborate_Body;
Comment_On_Normal_Passing : Boolean := True;
-- Do we say anything if the task completed normally?
type Termination_Event is record
Cause : Cause_Of_Termination;
Departed : Task_Id;
Event : Exception_Id;
end record;
package Termination_Events is new Ada.Containers.Vectors
(Positive, Termination_Event);
protected Writer is
procedure Note_Passing
(Cause : Cause_Of_Termination;
Departed : Task_Id;
Event : Exception_Occurrence);
entry Get_Event (Next : out Termination_Event);
private
Stored_Events : Termination_Events.Vector;
end Writer;
end Obituary;
As a minor refinement we add the option to not print announcements for normal completions, for those applications that allow task completion.
We must declare the generic container instantiation outside the protected object, an unfortunate limitation of protected objects. We would prefer that clients have no compile-time visibility to it, since it is an implementation artifact.
The updated package body is straightforward:
package body Obituary is
protected body Writer is
------------------
-- Note_Passing --
------------------
procedure Note_Passing
(Cause : Cause_Of_Termination;
Departed : Task_Id;
Event : Exception_Occurrence)
is
begin
if Cause = Normal and then
not Comment_On_Normal_Passing
then
return;
else
Stored_Events.Append
(Termination_Event'(Cause,
Departed,
Exception_Identity (Event)));
end if;
end Note_Passing;
---------------
-- Get_Event --
---------------
entry Get_Event (Next : out Termination_Event)
when
not Stored_Events.Is_Empty
is
begin
Next := Stored_Events.First_Element;
Stored_Events.Delete_First;
end Get_Event;
end Writer;
begin -- optional package executable part
Set_Dependents_Fallback_Handler (Writer.Note_Passing'Access);
end Obituary;
In the body of Note_Passing, we store the Exception_Id for the
exception occurrence indicated by Event. That exception occurrence need
not be active by the time the task reads the Id for that occurrence.
A new child package declares the task that prints the termination information:
package Obituary.Output is
pragma Elaborate_Body;
task Printer;
end Obituary.Output;
In the package body, the task body iteratively suspends on the call to
Writer.Get_Event, waiting for a termination handler to make the
termination data available. Once it returns from the call, if ever, it simply
prints the information and awaits further events:
with Ada.Text_IO; use Ada.Text_IO;
package body Obituary.Output is
-------------
-- Printer --
-------------
task body Printer is
Next : Termination_Event;
begin
loop
Writer. Get_Event (Next);
case Next.Cause is
when Normal =>
Put_Line (Image (Next.Departed) & " died, naturally.");
-- What a difference that comma makes!
Put_Line ("We had nothing to do with it.");
when Abnormal =>
Put_Line (Image (Next.Departed) &
" had a terrible accident.");
Put_Line ("We're sorry it had to come to that.");
when Unhandled_Exception =>
Put_Line (Image (Next.Departed) &
" reacted badly to " &
Exception_Name (Next.Event));
Put_Line ("Really, really badly.");
end case;
end loop;
end Printer;
end Obituary.Output;
We declared this task in a child package because one can view the
Printer and the Writer as parts of a single subsystem, but that
structure isn't necessary. An unrelated application task could just as easily
retrieve the information stored by the protected Writer object.
Here is a sample demonstration main procedure, a simple test to ensure that termination due to task abort is captured and displayed:
with Obituary.Output; pragma Unreferenced (Obituary.Output);
-- otherwise neither package is in the executable
procedure Demo_Fallback_Handler_Abort is
task Worker;
task body Worker is
begin
loop -- ensure not already terminated when aborted
delay 0.0; -- yield the processor
end loop;
end Worker;
begin
abort Worker;
end Demo_Fallback_Handler_Abort;
Note that the nested task would not be accepted under the Ravenscar or Jorvik profiles because those profiles require tasks to be declared at the library level, but that can easily be addressed.
When this demo main is run, the output looks like this:
worker_00000174BC68A570 had a terrible accident.
We're sorry it had to come to that.
The actual string representing the task identifier will vary with the implementation.
You'll have to use control-c (or whatever is required on your host) to end the
program because the Printer task in Obituary.Output runs forever.
Many applications run forever so that isn't necessarily a problem. That could
be addressed if need be.
Pros¶
The facility provided by package Ada.Task_Termination allows developers
to respond in any way required to task termination. The three causes, normal
completion, unhandled exceptions, and task abort are all supported.
Significantly, no source code in application tasks is required for the
termination support to be applied, other than the isolated calls to set the
handlers.
Cons¶
On a bare metal target there may be restrictions that limit the usefulness of the facility. For example, on targets that apply the Ravenscar or Jorvik profiles, task abort is not included in the profile and tasks are never supposed to terminate for any reason, including normally. Independent of the profiles, some run-time libraries may not support exception propagation, or even any exception semantics at all.
Relationship With Other Idioms¶
None.
Notes¶
If you did want to use a generic package to define a task type that is resilient to unhandled exceptions, you could do it like this:
with System;
with Ada.Exceptions; use Ada.Exceptions;
generic
type Task_Local_State is limited private;
with procedure Initialize (This : out Task_Local_State);
with procedure Update (This : in out Task_Local_State);
with procedure Respond_To_Exception
(Current_State : in out Task_Local_State;
Error : Exception_Occurrence);
package Resilient_Workers is
task type Worker
(Urgency : System.Priority := System.Default_Priority)
with
Priority => Urgency;
end Resilient_Workers;
package body Resilient_Workers is
task body Worker is
State : Task_Local_State;
begin
Recovery : loop
begin
Initialize (State);
Normal : loop
Update (State);
-- The call above is expected to return, ie
-- this loop is meant to iterate
end loop Normal;
exception
when Error : others =>
Respond_To_Exception (State, Error);
end;
end loop Recovery;
end Worker;
end Resilient_Workers;
Although this code looks useful, in practice it has issues.
First, in procedure Initialize, the formal parameter mode may be a
problem. You might need to change the parameter mode from mode out to mode
in-out instead, because recovery from unhandled exceptions will result in
another call to Initialize. Mode out makes sense for the first time
Initialize is called, but does it make sense for all calls after that?
It depends on the application's procedures. The behavior of Update may
be such that local state should only partially be reset in subsequent calls to
Initialize.
Furthermore, if Initialize must only perform a partial initialization on
subsequent calls, the procedure must keep track of the number of calls. That
requires a variable declared external to the body of Initialize. The
additional complexity is unfortunate. We could perhaps mitigate this problem by
having two initialization routines passed to the instantiation: one for full
initialization, called only once with mode out for the state, and one for
partial initialization, called on each iteration of the Recovery loop
with mode in-out for the state:
package body Resilient_Workers is
task body Worker is
State : Task_Local_State;
begin
Fully_Initialize (State);
Recovery : loop
begin
Normal : loop
Update (State);
-- The call above is expected to return, i.e.
-- this loop is meant to iterate
end loop Normal;
exception
when Error : others =>
Respond_To_Exception (State, Error);
end;
Partially_Initialize (State);
end loop Recovery;
end Worker;
end Resilient_Workers;
If both application initialization routines happen to do the same thing, we'd
like the developer to be able to pass the same application procedure to both
generic formal procedures Fully_Initialize and
Partially_Initialize in the instantiation. But that wouldn't compile
because the parameter modes don't match.
Then there's the question of the nature of the task. Is it periodic, or
sporadic, or free running? If it is periodic, we need a delay statement in the
Normal loop to suspend the task for the required period. The generic's
task body doesn't do that. The actual procedure passed to Update could
do the delay, but now, like a single version of Initialize required to
do both partial and full initialization, it needs additional state declared
external to the procedure body (for the Time variable used by the
absolute delay statement).
Finally, the single generic formal type used to represent the task's local
state can be awkward. Having one type for a task's total state is unusual,
and aggregating otherwise unrelated types into one isn't good software
engineering and doesn't reflect the application domain. Nor is it
necessarily trivial to create one type representing a set of distinct
variables. For example, some of these stand-alone variables could be
objects of indefinite types. Different task objects of a given task type
might not agree on those objects' constraints. Furthermore, that
awkwardness extends to the procedures that use that single object, in that
every procedure except for Initialize will likely ignore parts of
it.
In summary, the problems are likely more problematic than this generic is worth.
Footnotes