The debugger enables you to control the relative execution of threads
to diagnose problems of the kind shown in Example 16-1. In this case,
you can suspend the execution of the initial thread and let the worker
threads complete their computations so that they will be waiting on the
condition variable at the time of broadcast. The following procedure
explains how:
Example 16-2 Sample Ada Tasking Program |
1 --Tasking program that demonstrates various tasking conditions.
2
3 with TEXT_IO; use TEXT_IO;
4 procedure TASK_EXAMPLE is (1)
5
6 pragma TIME_SLICE(0.0); -- Disable time slicing. (2)
7
8 task type FATHER_TYPE is
9 entry START;
10 entry RENDEZVOUS;
11 entry BOGUS; -- Never accepted, caller deadlocks.
12 end FATHER_TYPE;
13
14 FATHER : FATHER_TYPE; (3)
15
16 task body FATHER_TYPE is
17 SOME_ERROR : exception;
18
19 task CHILD is (4)
20 entry E;
21 end CHILD;
22
23 task body CHILD is
24 begin
25 FATHER_TYPE.BOGUS; -- Deadlocks on call to its parent.
26 end CHILD; -- Whenever a task-type name
27 -- (here, FATHER_TYPE) is used within the
28 -- task body, the name denotes the task
29 -- currently executing the body.
30 begin -- (of FATHER_TYPE body)
31
32 accept START do
33 -- Main program is now waiting for this rendezvous completion,
34 -- and CHILD is suspended when it calls the entry BOGUS.
35
36 null;
37 <<B1>> end START;
38
39 PUT_LINE("FATHER is now active and"); (5)
40 PUT_LINE("is going to rendezvous with main program.");
41
42 for I in 1..2 loop
43 select
44 accept RENDEZVOUS do
45 PUT_LINE("FATHER now in rendezvous with main program");
46 end RENDEZVOUS;
47 or
48 terminate;
49 end select;
50
51 if I = 2 then
52 raise SOME_ERROR;
53 end if;
54 end loop;
55
56 exception
57 when OTHERS =>
58 -- CHILD is suspended on entry call to BOGUS.
59 -- Main program is going to delay while FATHER terminates.
60 -- Mother in suspended state with "Not yet activated" sub state.
61<<B2>> abort CHILD;
62 -- CHILD is now abnormal due to the abort statement.
63
64
65<<B3>> raise; -- SOME_ERROR exception terminates
66 FATHER.
67 end FATHER_TYPE; (6)
68
69 task MOTHER is (7)
70 entry START;
71 pragma PRIORITY (6);
72 end MOTHER;
73
74 task body MOTHER is
75 begin
76 accept START;
77 -- At this point, the main program is waiting for its dependents
78 -- (FATHER and MOTHER) to terminate. FATHER is terminated.
79
80 null;
81<<B4>> end MOTHER;
82
83 begin -- (of TASK_EXAMPLE)(8)
84 -- FATHER is suspended at accept start, and
85 -- CHILD is suspended in its deadlock.
86 -- Mother in suspended state with "Not yet activated" sub state.
87<<B5>> FATHER.START; (9)
88 -- FATHER is suspended at the 'select' or 'terminate' statement.
89
90
91<<B6>> FATHER.RENDEZVOUS;
92 FATHER.RENDEZVOUS; (10)
93 loop (11)
94 -- This loop causes the main program to busy wait for termination of
95 -- FATHER, so that FATHER can be observed in its terminated state.
96 if FATHER'TERMINATED then
97 exit;
98 end if;
99 delay 10.0; -- 10.0 so that MOTHER is suspended
100 end loop; -- at the 'accept' statement (increases determinism).
101
102 -- FATHER has terminated by now with an unhandled
103 -- exception, and CHILD no longer exists because its
104 -- master (FATHER) has terminated. Task MOTHER is ready.
105<<B7>> MOTHER.START; (12)
106 -- The main program enters a wait-for-dependents state
107 -- so that MOTHER can finish executing.
108 end TASK_EXAMPLE; (13)
|
Key to Example 16-2:
- After all of the Ada library packages are
elaborated (in this case, TEXT_IO), the main program is automatically
called and begins to elaborate its declarative part (lines 5 through
68).
- To ensure repeatability from run to run, the
example uses no time slicing The 0.0 value for the pragma TIME_SLICE
documents that the procedure TASK_EXAMPLE needs to have time slicing
disabled.
On Alpha processors, pragma TIME_SLICE (0.0) must be used to
disable time slicing.
- Task object FATHER is elaborated, and a task
designated %TASK 3 is created. FATHER has no pragma PRIORITY, and thus
assumes a default priority. FATHER (%TASK 3) is created in a suspended
state and is not activated until the beginning of the statement part of
the main program (line 69), in accordance with Ada rules. The
elaboration of the task body on lines 16 through 67 defines the
statements that tasks of type FATHER_TYPE will execute.
- Task FATHER declares a single task named
CHILD (line 19). A single task represents both a task object and an
anonymous task type. Task CHILD is not created or activated until
FATHER is activated.
- The only source of asynchronous system traps
(ASTs) is this series of TEXT_IO.PUT_LINE statements (I/O completion
delivers ASTs).
- The task FATHER is activated while the main
program waits. FATHER has no pragma PRIORITY and this assumes a default
priority of 7. (See the DEC Ada Language Reference Manual for the rules about default
priorities.) FATHER's activation consists of the elaboration of lines
16 through 29.
When task FATHER is activated, it waits while its
task CHILD is activated and a task designated %TASK 4 is created. CHILD
executes one entry call on line 25, and then deadlocks because the
entry is never accepted (see Section 16.7.1).
Because time slicing
is disabled and there are no higher priority tasks to be run, FATHER
will continue to execute past its activation until it is blocked at the
ACCEPT statement at line 32.
- A single task, MOTHER, is defined, and a task
designated %TASK 2 is created. The pragma PRIORITY gives MOTHER a
priority of 6.
- The task MOTHER begins its activation and
executes line 74. After MOTHER is activated, the main program (%TASK 1)
is eligible to resume its execution. Because %TASK 1 has the default
priority 7, which is higher than MOTHER's priority, the main program
resumes execution.
- This is the first rendezvous the main program
makes with task FATHER. After the rendezvous FATHER will suspend at the
SELECT with TERMINATE statement at line 43.
- At the third rendezvous with FATHER, FATHER
raises the exception SOME_ERROR on line 52. The handler on line 57
catches the exception, aborts the suspended CHILD task, and then
reraises the exception; FATHER then terminates.
- A loop with a delay statement ensures that
when control reaches line 102, FATHER has executed far enough to be
terminated.
- This entry call ensures that MOTHER does not
wait forever for its rendezvous on line 76. MOTHER executes the accept
statement (which involves no other statements), the rendezvous is
completed, and MOTHER is immediately switched off the processor at line
77 because its priority is only 6.
- After its rendezvous with MOTHER, the main
program (%TASK 1) executes lines 106 through 108. At line 108, the main
program must wait for all its dependent tasks to terminate. When the
main program reaches line 108, the only nonterminated task is MOTHER
(MOTHER cannot terminate until the null statement at
line 80 has been executed). MOTHER finally executes to its completion
at line 81. Now that all tasks are terminated, the main program
completes its execution. The main program then returns and execution
resumes with the command line interpreter.
16.3 Specifying Tasks in Debugger Commands
A task is an entity that executes in parallel with
other tasks. A task is characterized by a unique task ID (see
Section 16.3.3), a separate stack, and a separate register set.
The current definition of the active task and the visible task
determine the context for manipulating tasks. See Section 16.3.1.
When specifying tasks in debugger commands, you can use any of the
following forms:
- A task (thread) name as declared in the program (for example,
FATHER in Section 16.2.2) or a language expression that yields a task
value. Section 16.3.2 describes Ada language expressions for tasks.
- A task ID (for example, %TASK 2). See Section 16.3.3.
- A task built-in symbol (for example, %ACTIVE_TASK). See
Section 16.3.4.
16.3.1 Definition of Active Task and Visible Task
The active task is the task that runs when a STEP, GO, CALL, or EXIT
command executes. Initially, it is the task in which execution is
suspended when the program is brought under debugger control. To change
the active task during a debugging session, use the SET TASK/ACTIVE
command.
Note
The SET TASK/ACTIVE command does not work for POSIX Threads (on OpenVMS
Alpha and Integrity server systems) or for Ada on OpenVMS Alpha and
Integrity server systems, the tasking for which is implemented via
POSIX Threads. Instead of SET TASK/ACTIVE, use the SET TASK/VISIBLE
command on POSIX Threads for query-type actions. Or, to gain control to
step through a particular thread, use a strategic placement of
breakpoints.
|
The following command makes the task named CHILD the active task:
DBG> SET TASK/ACTIVE CHILD
|
The visible task is the task whose stack and register
set are the current context that the debugger uses when looking up
symbols, register values, routine calls, breakpoints, and so on. For
example, the following command displays the value of the variable
KEEP_COUNT in the context of the visible task:
Initially, the visible task is the active task. To change the visible
task, use the SET TASK/VISIBLE command. This enables you to look at the
state of other tasks without affecting the active task.
You can specify the active and visible tasks in debugger commands by
using the built-in symbols %ACTIVE_TASK and %VISIBLE_TASK, respectively
(see Section 16.3.4).
See Section 16.5 for more information about using the SET TASK command
to modify task characteristics.
16.3.2 Ada Tasking Syntax
You declare a task either by declaring a single task or by declaring an
object of a task type. For example:
-- TASK TYPE declaration.
--
task type FATHER_TYPE is
...
end FATHER_TYPE;
task body FATHER_TYPE is
...
end FATHER_TYPE;
-- A single task.
--
task MOTHER is
...
end MOTHER;
task body MOTHER is
...
end MOTHER;
|
A task object is a data item that contains a task
value. A task object is created when the program elaborates a single
task or task object, when you declare a record or array containing a
task component, or when a task allocator is evaluated. For example:
-- Task object declaration.
--
FATHER : FATHER_TYPE;
-- Task object (T) as a component of a record.
--
type SOME_RECORD_TYPE is
record
A, B: INTEGER;
T : FATHER_TYPE;
end record;
HAS_TASK : SOME_RECORD_TYPE;
-- Task object (POINTER1) via allocator.
--
type A is access FATHER_TYPE;
POINTER1 : A := new FATHER_TYPE;
|
A task object is comparable to any other object. You refer to a task
object in debugger commands either by name or by path name. For example:
DBG> EXAMINE FATHER
DBG> EXAMINE FATHER_TYPE$TASK_BODY.CHILD
|
When a task object is elaborated, a task is created by the Compaq Ada
Run-Time Library, and the task object is assigned its task value. As
with other Ada objects, the value of a task object is undefined before
the object is initialized, and the results of using an uninitialized
value are unpredictable.
The task body of a task type or single task is
implemented in Compaq Ada as a procedure. This procedure is called by
the Compaq Ada Run-Time Library when a task of that type is activated.
A task body is treated by the debugger as a normal Ada procedure,
except that it has a specially constructed name.
To specify the task body in a debugger command, use the following
syntax to refer to tasks declared as task types:
task-type-identifier$TASK_BODY
|
Use the following syntax to refer to single tasks:
task-identifier$TASK_BODY
|
For example:
DBG> SET BREAK FATHER_TYPE$TASK_BODY
|
The debugger does not support the task-specific Ada attributes
T'CALLABLE, E'COUNT, T'STORAGE_SIZE, and T'TERMINATED, where T is a
task type and E is a task entry (see the Compaq Ada documentation for
more information on these attributes). You cannot enter commands such
as EVALUATE CHILD'CALLABLE. However, you can get the information
provided by each of these attributes with the debugger SHOW TASK
command. For more information, see Section 16.4.
16.3.3 Task ID
A task ID is the number assigned to a task when it is
created by the tasking system. The task ID uniquely identifies a task
during the entire execution of a program.
A task ID has the following syntax, where n is a positive
decimal integer:
You can determine the task ID of a task object by evaluating or
examining the task object. For example (using Ada path-name syntax):
DBG> EVALUATE FATHER
%TASK 3
DBG> EXAMINE FATHER
TASK_EXAMPLE.FATHER: %TASK 3
|
If the programming language does not have built-in tasking services,
you must use the EXAMINE/TASK command to obtain the task ID of a task.
Note that the EXAMINE/TASK/HEXADECIMAL command, when applied to a task
object, yields the hexadecimal task value. The task value is the
address of the task (or thread) control block of that task. For example
(Ada example):
DBG> EXAMINE/HEXADECIMAL FATHER
TASK_EXAMPLE.FATHER: 0085A448
DBG>
|
The SHOW TASK/ALL command enables you to identify the task IDs that
have been assigned to all currently existing tasks. Some of these
existing tasks may not be immediately familiar to you for the following
reasons:
- A SHOW TASK/ALL display includes tasks created by subsystems such
as POSIX Threads, Remote Procedure Call services, and the C Run-Time
Library, not just the tasks associated with your application.
- A SHOW TASK/ALL display includes task ID assignments that depend on
your operating system, your tasking service, and the generating
subsystem. The same tasking program, run on different systems or
adjusted for different services, will not identify tasks with the same
decimal integer. The only exception is %TASK 1, which all systems and
services assign to the task that executes the main program.
The following examples are derived from Example 16-1 and
Example 16-2, respectively:
DBG> SHOW TASK/ALL
task id state hold pri substate thread_object
%TASK 1 READY HOLD 12 Initial thread
%TASK 2 SUSP 12 Condition Wait THREAD_EX1\main\threads[0].field1
%TASK 3 SUSP 12 Condition Wait THREAD_EX1\main\threads[1].field1
DBG>
|
DBG> SHOW TASK/ALL
task id state hold pri substate thread_object
%TASK 1 7 SUSP Entry call SHARE$ADARTL+393712
* %TASK 3 7 READY TASK_EXAMPLE.FATHER
%TASK 4 7 SUSP Entry call TASK_EXAMPLE.FATHER_TYPE$TASK_BODY.CHILD
%TASK 2 6 SUSP Not yet activated TASK_EXAMPLE.MOTHER
|
You can use task IDs to refer to nonexistent tasks in debugger
conditional statements. For example, if you ran your program once, and
you discovered that %TASK 2 and 3 were of interest, you could enter the
following commands at the beginning of your next debugging session
before %TASK 2 or 3 was created:
DBG> SET BREAK %LINE 30 WHEN (%ACTIVE_TASK=%TASK 2)
DBG> IF (%CALLER=%TASK 3) THEN (SHOW TASK/FULL)
|
You can use a task ID in certain debugger commands before the task has
been created without the debugger reporting an error (as it would if
you used a task object name before the task object came into
existence). A task does not exist until the task is created. Later the
task becomes nonexistent sometime after it terminates. A nonexistent
task never appears in a debugger SHOW TASK display.
Each time a program runs, the same task IDs are assigned to the same
tasks so long as the program statements are executed in the same order.
Different execution orders can result from ASTs (caused by delay
statement expiration or I/O completion) being delivered in a different
order. Different execution orders can also result from time slicing
being enabled. A given task ID is never reassigned during the execution
of the program.