OpenVMS Debugger Manual


Previous Contents Index

  1. The first few statements of main() initialize the synchronization objects used by the threads, as well as the predicate that is to be associated with the condition variable. The synchronization objects are initialized with the default attributes. The condition variable predicate is initialized such that a thread that is looping on it will continue to loop. At this point in the program, a SHOW TASK/ALL display lists %TASK 1.
  2. The worker threads %TASK 2 and %TASK 3 are created. Here the created threads execute the same start routine (worker_routine) and can also reuse the same call to pthread_create with a slight change to store the different thread IDs. The threads are created using the default attributes and are passed an argument that is not used in this example.
  3. The predicate associated with the condition variable is cleared in preparation to broadcast. This ensures that any thread awaking off the condition variable has received a valid wake-up and not a spurious one. Clearing the predicate also prevents any new arrivals from waiting on the condition variable because it has been broadcast or signaled upon. (The desired effect depends on correct coding being used for the condition wait call at line 3893, which is not the case in this example.)
  4. The initial thread issues the broadcast call almost immediately, so that none of the worker threads should yet be at the condition wait. A broadcast should wake any threads currently waiting on the condition variable.
    As the programmer, you should ensure that a broadcast is seen by either ensuring that all threads are waiting on the condition variable at the time of broadcast or ensuring that an associated predicate is used to flag that the broadcast has already happened. (These measures have been left out of this example on purpose.)
  5. The initial thread attempts to join with the worker threads to ensure that they exited properly.
  6. When the worker threads execute worker_routine, they spend time doing many computations. This allows the initial thread to broadcast on the condition variable before either of the worker threads is waiting on it.
  7. The worker threads then proceed to execute a pthread_cond_wait call by performing locks around the call as required. It is here that both worker threads will block, having missed the broadcast. A SHOW TASK/ALL command entered at this point will show both of the worker threads waiting on a condition variable. (After the program is deadlocked in this way, you must press Ctrl/C to return control to the debugger.)

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:

  1. At the start of the debugging session, set a breakpoint on line 3836 to suspend execution of the initial thread just before broadcast.
  2. Enter the GO command to execute the initial thread and create the worker threads.
  3. At this breakpoint, which causes the execution of all threads to be suspended, put the initial thread on hold with the SET TASK/HOLD %TASK 1 command.
  4. Enter the GO command to let the worker threads continue execution. The initial thread is on hold and cannot execute.
  5. When the worker threads block on the condition variable, press Ctrl/C to return control to the debugger at that point. A SHOW TASK/ALL command should indicate that both worker threads are suspended in a condition wait substate. (If not, enter GO to let the worker threads execute, press Ctrl/C, and enter SHOW TASK/ALL, repeating the sequence until both worker threads are in a condition wait substate.)
  6. Enter the SET TASK/NOHOLD %TASK command 1 and then the GO command to allow the initial thread to resume execution and broadcast. This will enable the worker threads to join and terminate properly.

16.2.2 Sample Ada Tasking Program

Example 16-2 demonstrates a number of common errors that you may encounter when debugging tasking programs. This is an example from an OpenVMS Alpha system running the OpenVMS Debugger. The calls to procedure BREAK in the example mark points of interest where breakpoints could be set and the state of each task observed. If you ran the example under debugger control, you could enter the following commands to set breakpoints at each call to the procedure BREAK and display the current state of each task:


DBG> SET BREAK %LINE  37 DO (SHOW TASK/ALL)
DBG> SET BREAK %LINE  61 DO (SHOW TASK/ALL)
DBG> SET BREAK %LINE  65 DO (SHOW TASK/ALL)
DBG> SET BREAK %LINE  81 DO (SHOW TASK/ALL)
DBG> SET BREAK %LINE  87 DO (SHOW TASK/ALL)
DBG> SET BREAK %LINE  91 DO (SHOW TASK/ALL)
DBG> SET BREAK %LINE 105 DO (SHOW TASK/ALL)

The program creates four tasks:

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:

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. The only source of asynchronous system traps (ASTs) is this series of TEXT_IO.PUT_LINE statements (I/O completion delivers ASTs).
  6. 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.
  7. A single task, MOTHER, is defined, and a task designated %TASK 2 is created. The pragma PRIORITY gives MOTHER a priority of 6.
  8. 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.
  9. 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.
  10. 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.
  11. A loop with a delay statement ensures that when control reaches line 102, FATHER has executed far enough to be terminated.
  12. 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.
  13. 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:

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:


DBG> EXAMINE KEEP_COUNT

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:


%TASK n

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:

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.


Previous Next Contents Index