@@ -18,7 +18,7 @@ task completes, again abnormally.
1818
1919Whatever the cause, once completed a task will (eventually) terminate, and it
2020does 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
2222their run-time library [#f1 ]_, but the language standard does not require it
2323and most vendors |mdash | if not all |mdash | do not.
2424
@@ -30,7 +30,7 @@ implementing it, how can the requirement best be met?
3030Implementation
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
3434define the announcement or logging response as an exception handler located in
3535the 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,
5151in 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
5354previously covered, so in the above it covers all exceptions. Specific
5455exceptions also could be covered, but the :ada: `others ` choice should be
5556included (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
101102the 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
104105inside a block statement. The task body's exception handler section becomes
105106part 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
127128after the handler executes, and so far there's nothing after that block
128129statement. 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
131132statements after a handler executes, and hence the task from completing. This
132133is accomplished by wrapping the new block statement inside a new loop
133134statement. 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
156156an unhandled exception reaching this level, the :ada: `Normal ` loop is never
157157exited 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
179331statements.
180332
181333Aborting 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
251403Termination handlers apply either to a specific task or to a group of related
252404tasks, 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
255408Clients call procedure :ada: `Set_Specific_Handler ` to apply the protected
256409procedure designated by :ada: `Handler ` to the task with the specific
@@ -346,10 +499,11 @@ convenient. This executable part calls procedure
346499Because this call happens during library unit elaboration, it sets the
347500fall-back handler for all the tasks in the partition (the program). The effect
348501is 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
354508The call to :ada: `Set_Dependents_Fallback_Handler ` need not occur in this
355509particular 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
424578For 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
426580because a task can make blocking calls.
427581
428582First, 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+
535693A 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
775933absolute delay statement).
776934
777935Finally, 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
784947In summary, the problems are likely more problematic than this generic is
785948worth.
0 commit comments