Skip to content

Commit 219edd6

Browse files
pat-rogersgusthoff
authored andcommitted
Revisions in response to Steve's review
Several small refinements, one big addition explaining storage exhaustion.
1 parent 7a147b7 commit 219edd6

File tree

1 file changed

+212
-49
lines changed

1 file changed

+212
-49
lines changed

content/courses/ada-in-practice/chapters/silent_task_termination.rst

Lines changed: 212 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ task completes, again abnormally.
1818

1919
Whatever the cause, once completed a task will (eventually) terminate, and it
2020
does this silently |mdash| there is no notification or logging of the
21-
termination to the external environment. A vendor could support notification by
21+
termination to the external environment. A vendor could support notification via
2222
their run-time library [#f1]_, but the language standard does not require it
2323
and most vendors |mdash| if not all |mdash| do not.
2424

@@ -30,7 +30,7 @@ implementing it, how can the requirement best be met?
3030
Implementation
3131
--------------
3232

33-
For unhandled exceptions, the simplest solution to silent termination is to
33+
For unhandled exceptions, the simplest approach to silent termination is to
3434
define the announcement or logging response as an exception handler located in
3535
the task body exception handler part:
3636

@@ -44,12 +44,13 @@ the task body exception handler part:
4444
-- ...
4545
exception
4646
when Error : others => -- last wishes
47-
Put_Line ("Task T terminated due to " & Exception_Name (Error));
47+
Put_Line ("Task Worker terminated due to " & Exception_Name (Error));
4848
end Worker;
4949
5050
A handler at this level expresses the task's *last wishes* prior to completion,
5151
in this case printing the names of the task and the active exception to
52-
:ada:`Standard_Output`. The :ada:`others` choice covers all exceptions not
52+
:ada:`Standard_Output`. (We could print the associated exception message too, if desired.)
53+
The :ada:`others` choice covers all exceptions not
5354
previously covered, so in the above it covers all exceptions. Specific
5455
exceptions also could be covered, but the :ada:`others` choice should be
5556
included (at the end) to ensure no exception occurrence can be missed.
@@ -87,7 +88,7 @@ handler:
8788
end loop Normal;
8889
exception
8990
when Error : others => -- last wishes
90-
Put_Line ("Task T terminated due to " &
91+
Put_Line ("Task Worker terminated due to " &
9192
Exception_Name (Error));
9293
end Worker;
9394
@@ -100,7 +101,7 @@ statements*). We want to prevent the thread of control reaching that end
100101
|mdash| which would happen if any handler there ever executed |mdash| because
101102
the task would then complete.
102103

103-
Therefore, for the first additional construct, we first wrap the existing code
104+
Therefore, we first wrap the existing code
104105
inside a block statement. The task body's exception handler section becomes
105106
part of the block statement rather than at the top level of the task:
106107

@@ -115,7 +116,7 @@ part of the block statement rather than at the top level of the task:
115116
end loop Normal;
116117
exception
117118
when Error : others => -- last wishes
118-
Put_Line ("Task T terminated due to " &
119+
Put_Line ("Task Worker terminated due to " &
119120
Exception_Name (Error));
120121
end;
121122
end Worker;
@@ -127,7 +128,7 @@ semantically. The task will still complete because the block statement exits
127128
after the handler executes, and so far there's nothing after that block
128129
statement. We need to make one more addition.
129130

130-
The second (final) addition prevents reaching the end of the sequence of
131+
The second (and final) addition prevents reaching the end of the sequence of
131132
statements after a handler executes, and hence the task from completing. This
132133
is accomplished by wrapping the new block statement inside a new loop
133134
statement. We name this outermost loop :ada:`Recovery`:
@@ -144,8 +145,7 @@ statement. We name this outermost loop :ada:`Recovery`:
144145
end loop Normal;
145146
exception
146147
when Error : others =>
147-
Put_Line ("Task T terminated due to " &
148-
Exception_Name (Error));
148+
Put_Line (Exception_Name (Error) & " handled in task Worker");
149149
end;
150150
end loop Recovery;
151151
end Worker;
@@ -156,26 +156,178 @@ The thread of control then continues at the top of the loop. Of course, absent
156156
an unhandled exception reaching this level, the :ada:`Normal` loop is never
157157
exited in the first place.
158158

159-
These two additions ensure that :ada:`Worker` never terminates due to an
160-
unhandled exception raised during execution of the task's sequence of
161-
statements. Note that an exception raised during elaboration of the task
162-
body's declarative part is not handled by the approach, or any other approach
163-
at this level, because the exception is propagated immediately to the master
164-
of this task. Such a task never reaches the handled sequence of statements in
165-
the first place.
166-
167-
That works, but the state initialization requires some thought. As shown above,
168-
full initialization is performed again when the :ada:`Recovery` loop circles
169-
back around to the top of the loop. As a result, the *normal* processing in
170-
:ada:`Do_Actual_Work` must be prepared for suddenly encountering completely
171-
different state, i.e., a restart to the initial state. If that is not feasible
172-
the call to :ada:`Initialize_State` could be moved outside, prior to the start
173-
of the :ada:`Recovery` loop, so that it only executes once. Perhaps a different
174-
initialization procedure could be called after the exception handler to do
175-
partial initialization. Whether or not that will suffice depends on the
176-
application.
177-
178-
However, these solutions do not address task termination due to task abort
159+
These two additions ensure that |mdash| with one caveat |mdash| :ada:`Worker`
160+
never terminates due to an unhandled exception raised during execution of the
161+
task's sequence of statements.
162+
163+
Note that an exception raised during elaboration of the task body's
164+
declarative part is not handled by the approach, or any other approach at this
165+
level, because the exception is propagated immediately to the master of the
166+
task. Such a task never reaches the handled sequence of statements in the
167+
first place.
168+
169+
The caveat concerns the language-defined exception :ada:`Storage_Error`. This
170+
exception requires special consideration because the reasons for raising it
171+
include exhausting the storage required for execution itself.
172+
173+
There are a couple of scenarios to consider.
174+
175+
The first scenario is task activation, i.e., creation. Initial task activation
176+
involves execution (in the tasking part of the run-time library) before the
177+
sequence of steps is reached. Hence task activation for the :ada:`Worker` task
178+
could fail due to an insufficient initial storage allocation. But because that
179+
failure happens before the block statement is entered it doesn't really apply
180+
to the caveat above.
181+
182+
The second scenario involves execution within the task's actual sequence of
183+
statements. Therefore it does apply to the caveat above. Here's why.
184+
185+
When called, the execution of a given subprogram requires a representation in
186+
storage, often known as a "frame." Because subprogram calls and their returns
187+
can be seen as a series of stack pushes and pops, the representation for
188+
execution is typically via a stack of these frames. Calls cause stack frame
189+
pushes, creating new frames on the stack, and returns cause stack pops,
190+
reclaiming the frames. On exit, execution returns to the caller, so the
191+
previous top of the stack is now the active frame. Representation as a stack
192+
of frames works well so it is very common. (Functions returning values of
193+
unconstrained types are problematic because the size of the result isn't known
194+
at the point of the call, so the required frame size isn't known when the push
195+
occurs. Solutions vary, but that's a topic for another day.)
196+
197+
Now, suppose the task's sequence of statements includes a long series of
198+
subprogram calls, in which one subprogram calls another, and that one calls
199+
another, and so on, and none of these calls has yet returned. Eventually, of
200+
course, the dynamic call chain will end because the calls will return, at
201+
least in normal code. But let's suppose that the call chain is long and the most
202+
recent call has not yet returned to the caller.
203+
204+
In that case it is possible for one more call to exhaust the storage available
205+
for that task's execution. You can easily construct such a chain by calling an
206+
infinitely recursive procedure or function:
207+
208+
.. code-block:: ada
209+
210+
procedure P;
211+
212+
procedure P is
213+
begin
214+
P;
215+
end P;
216+
217+
When executing on a host OS it might take a very long time for a call to
218+
:ada:`P` to exhaust available storage, maybe longer than you'd be willing to
219+
wait. But on an embedded system, where physical storage is limited and there's
220+
no virtual memory, it might not take long at all.
221+
222+
Now, you might think that you don't use recursion, much less infinitely
223+
recursive routines, so this problem doesn't apply to you. But recursion is just an
224+
easy illustration. How long a call chain is too long? It depends on the memory
225+
resources available.
226+
227+
Moreover, exhaustion is not due only to the storage required for the
228+
call/return semantics. Frames include the representation of the local objects
229+
declared within the subprograms' declarative parts, if any.
230+
231+
.. code-block:: ada
232+
233+
procedure P;
234+
235+
procedure P is
236+
Local : Integer;
237+
begin
238+
Local := 0;
239+
P;
240+
end P;
241+
242+
Each execution of a call to :ada:`P` creates a semantically distinct instance
243+
of :ada:`Local`. A new frame containing the storage for each call's copy of
244+
:ada:`Local` implements that requirement nicely.
245+
246+
Of course, different subprograms usually declare different local objects, if
247+
they declare any at all. Because the storage required for these declarations
248+
varies, the corresponding frame sizes vary.
249+
250+
We can use that fact to reduce the length of the dynamic call chain required
251+
to illustrate storage exhaustion. The called subprograms will declare very large
252+
objects within their declarative parts. Hence each frame is
253+
correspondingly larger than if the subprogram declared nothing locally.
254+
Continuing the infinitely recursive subprogram example:
255+
256+
.. code-block:: ada
257+
258+
procedure P;
259+
260+
procedure P is
261+
type Huge_Component is array (Long_Long_Integer) of Long_Float;
262+
type Huge_Array is array (Long_Long_Integer) of Huge_Component;
263+
Local : Huge_Array;
264+
begin
265+
Local := (others => (others => 0.0));
266+
P;
267+
end P;
268+
269+
The size of the frame for an individual call to :ada:`P` will be very large
270+
indeed, if it is representable at all. Fewer calls will be required before
271+
:ada:`Storage_Error` is raised.
272+
273+
Now, with all that said, let's get back to this approach to silent termination.
274+
Here's the code again:
275+
276+
.. code-block:: ada
277+
278+
task body Worker is
279+
begin
280+
Recovery : loop
281+
begin
282+
Initialize_State;
283+
Normal : loop
284+
Do_Actual_Work;
285+
end loop Normal;
286+
exception
287+
when Error : others =>
288+
Put_Line (Exception_Name (Error) & " handled in task Worker.");
289+
end;
290+
end loop Recovery;
291+
end Worker;
292+
293+
At this point you might be thinking that :ada:`Storage_Error` would be caught
294+
by the :ada:`others` choice anyway, so this (long-winded) talk about stack
295+
frames and dynamic call chains is irrelevant. That's where the caveat comes
296+
into play.
297+
298+
Specifically, if there's insufficient storage remaining for execution to
299+
continue, how how do we know there's enough storage remaining to execute the
300+
exception handler? For that matter, how do we even know there's enough storage
301+
available for the run-time library to find the handler in the first place?
302+
Absent a storage analysis, we can't know with certainty.
303+
304+
Therefore, if the application matters, perform a worst-case storage analysis
305+
per task, including the exception handlers, and explicitly specify the tasks'
306+
stacks accordingly. For example:
307+
308+
.. code-block:: ada
309+
310+
task Worker with Storage_Size => System_Config.Worker_Storage;
311+
312+
We've defined the value as a constant named :ada:`Worker_Storage` declared in
313+
an application-defined package :ada:`System_Config`. All such values are
314+
declared in that package, for the sake of centralizing all the application's
315+
configuration parameters. We'd declare all the tasks' priorities there too.
316+
317+
Finally, although this approach works, the state initialization requires some
318+
thought.
319+
320+
As shown above, full initialization is performed again when the
321+
:ada:`Recovery` loop circles back around to the top of the loop. As a result,
322+
the *normal* processing in :ada:`Do_Actual_Work` must be prepared for suddenly
323+
encountering completely different state, i.e., a restart to the initial state.
324+
If that is not feasible the call to :ada:`Initialize_State` could be moved
325+
outside, prior to the start of the :ada:`Recovery` loop, so that it only
326+
executes once. Perhaps a different initialization procedure could be called
327+
after the exception handler to do partial initialization. Whether or not that
328+
will suffice depends on the application.
329+
330+
However, these approaches do not address task termination due to task abort
179331
statements.
180332

181333
Aborting tasks is both messy and expensive at run-time. If a task is updating
@@ -250,7 +402,8 @@ any designated protected procedure matches the parameter profile.
250402

251403
Termination handlers apply either to a specific task or to a group of related
252404
tasks, including potentially all tasks in the partition. Each task has one,
253-
both, or neither kind of handler. By default none apply.
405+
both, or neither kind of handler. By default none apply. (Unless a partition
406+
is part of a distributed program, a single partition constitutes an entire Ada program.)
254407

255408
Clients call procedure :ada:`Set_Specific_Handler` to apply the protected
256409
procedure designated by :ada:`Handler` to the task with the specific
@@ -346,10 +499,11 @@ convenient. This executable part calls procedure
346499
Because this call happens during library unit elaboration, it sets the
347500
fall-back handler for all the tasks in the partition (the program). The effect
348501
is global to the partition because library unit elaboration is invoked by the
349-
*environment task,* and the environment task is the master of all application
350-
tasks in the partition. Therefore, the fall-back handler is applied to the top
351-
of the task dependents hierarchy, and thus to all tasks. The application tasks
352-
need not do anything in their source code for the handler to apply to them.
502+
*environment task,* and the environment task is the ultimate master of all
503+
application tasks in a partition. Therefore, the fall-back handler is applied
504+
to the top of the task dependents hierarchy, and thus to all tasks. The
505+
application tasks need not do anything in their source code for the handler to
506+
apply to them.
353507

354508
The call to :ada:`Set_Dependents_Fallback_Handler` need not occur in this
355509
particular package body, or even in a package body at all. But because we want
@@ -413,16 +567,16 @@ ramifications momentarily. Here are the bodies for the two handlers:
413567
Set_Dependents_Fallback_Handler (Writer.Note_Passing'Access);
414568
end Obituary;
415569

416-
417-
Now, about those calls to :ada:`Ada.Text_IO.Put_Line`. Because of those calls,
418-
the bodies of procedures :ada:`Note_Passing` and :ada:`Dissemble` are not
419-
portable. The :ada:`Put_Line` calls are useful for illustration and will likely
420-
work as expected on a native OS. However, their execution is a bounded error
421-
and may do something else on other targets, including raising
422-
:ada:`Program_Error` if detected.
570+
Now, about those calls to :ada:`Ada.Text_IO.Put_Line`. Procedure
571+
:ada:`Put_Line` is a "potentially blocking" operation. Consequently, a call
572+
within a protected operation is a bounded error (see RM 9.5.1(8)) and the
573+
resulting execution is not portable. For example, the :ada:`Put_Line` calls
574+
will likely work as expected on a native OS. However, their execution may do
575+
something else on other targets, including raising :ada:`Program_Error` if
576+
detected. The GNAT bare-metal targets, for example, raise :ada:`Program_Error`.
423577

424578
For a portable approach, we move these two blocking calls to a new dedicated
425-
task and revise the protected object accordingly. That's a portable approach
579+
task and revise the protected object accordingly. That's portable
426580
because a task can make blocking calls.
427581

428582
First, we change :ada:`Obituary.Writer` to have a single protected procedure
@@ -505,7 +659,7 @@ The updated package body is straightforward:
505659
not Comment_On_Normal_Passing
506660
then
507661
return;
508-
else -- store all three causes and their info
662+
else
509663
Stored_Events.Append
510664
(Termination_Event'(Cause,
511665
Departed,
@@ -532,6 +686,10 @@ The updated package body is straightforward:
532686
Set_Dependents_Fallback_Handler (Writer.Note_Passing'Access);
533687
end Obituary;
534688

689+
In the body of :ada:`Note_Passing`, we store the :ada:`Exception_Id` for the
690+
exception occurrence indicated by :ada:`Event`. That exception occurrence need
691+
not be active by the time the task reads the Id for that occurrence.
692+
535693
A new child package declares the task that prints the termination information:
536694

537695
.. code:: ada no_button project=Courses.Ada_In_Practice.Silent_Task_Termination.Obituary_Updated
@@ -775,11 +933,16 @@ external to the procedure body (for the :ada:`Time` variable used by the
775933
absolute delay statement).
776934

777935
Finally, the single generic formal type used to represent the task's local
778-
state can be awkward. Having one type for a task's total state is unusual, and
779-
aggregating otherwise unrelated types into one isn't good software engineering
780-
and doesn't reflect the application domain. Furthermore, that awkwardness
781-
extends to the procedures that use that single object, in that every procedure
782-
except for :ada:`Initialize` will likely ignore parts of it.
936+
state can be awkward. Having one type for a task's total state is unusual,
937+
and aggregating otherwise unrelated types into one isn't good software
938+
engineering and doesn't reflect the application domain. Nor is it
939+
necessarily trivial to create one type representing a set of distinct
940+
variables. For example, some of these stand-alone variables could be
941+
objects of indefinite types. Different task objects of a given task type
942+
might not agree on those objects' constraints. Furthermore, that
943+
awkwardness extends to the procedures that use that single object, in that
944+
every procedure except for :ada:`Initialize` will likely ignore parts of
945+
it.
783946

784947
In summary, the problems are likely more problematic than this generic is
785948
worth.

0 commit comments

Comments
 (0)