!topic LSN on Task Identification in Ada 9X !key LSN-1040 on Task Identification in Ada 9X !reference MS-9.2(3);4.6 !reference MS-G.5.2;4.8 !reference LSN-1043 !from Bob Duff $Date: 92/10/14 13:10:04 $ $Revision: 1.3 $ !discussion This Language Study Note discusses task identification in Ada 9X. The concensus on task identification is that Ada 9X needs to define a standard data type that represents an assignable handle on a task. This LSN discusses two alternative mechanisms. In Ada 83, there are several operations that work for any object of any task type -- these operations don't care which specific task type the object belongs to. The abort statement works for an object of any task type. The 'TERMINATED and 'CALLABLE attributes work for an object of any task type. In Ada 9X, the Systems Programming and Real Time Annexes need to define additional operations that work for an object of any task type. An example is SET_PRIORITY. Following the ground rules for optional Annexes, SET_PRIORITY must be defined in a way that would be valid for a user or implementation to define -- we cannot define a new statement for this purpose. Hence, the current proposal to make SET_PRIORITY a procedure. Obviously, it is necessary for that procedure to work for any task object, no matter what its specific type. In Ada 9X, the Annexes obviously do not provide every feature that every real-time user will need. Instead, the user will need to design abstractions similar to those provided by the language. For example, many real-time users need the ability to write their own scheduler. In order to do so, it is necessary to write subprograms that work for any task object, no matter what its specific type. Similarly, it is necessary to build data structures that can store the handles of task objects, and these data structures need to be heterogeneous in the sense that they will simultaneously contain the handles of objects belonging to different specific task types. Note that in order to build data structures, task handles, whatever they are, must be assignable. But we do not wish to assign tasks themselves -- copying TCB's is disastrous. (See also LSN-1043, which discusses "inherently limited" types.) Ada 83 defines task types to be limited, and we're not changing that fact. That's why we're talking about handles. Whatever these assignable handles are, the above operations need to work for them. For example, if the user-defined scheduler stores a task handle in some data structure, and then later retrieves that handle, it should be possible to abort the task represented by that handle, or to change its priority. So far, I have said nothing controversial. The consensus is that the above functionality is necessary to the success of Ada 9X; what is at issue here is the mechanism for providing that functionality. ALTERNATIVE 1: THE HANDLE TYPE IS A MAGIC TYPE. In this alternative, there is a new data type, called TASK_ID, or perhaps TASK_HANDLE, that represents task handles. It is not limited, so assignment is allowed. It private, so that the run-time system can implement it in any way desired. The following attribute is defined for any task object T: T'IDENTITY This attribute yields a unique identifier of the task. T1'IDENTITY = T2'IDENTITY if and only if T1 and T2 are the same task. (E.g. T2 might be a parameter of a task type.) The value of this attribute is of type TASK_ID. All operations that are new to Ada 9X can be defined to use type TASK_ID as parameter and result types. For example, CURRENT_TASK would be a function whose result type is TASK_ID. However, operations that already exist in Ada 83 are defined to accept any object in the class of all tasks as a parameter. Any of the following sub-alternatives will solve this problem: Sub-Alternative A: There is some way to convert a value of type TASK_ID to a value of a given task type. This could be done with an attribute or with a magic generic. For example, the language could define this: generic type SOME_TASK_TYPE(<>) is limited private; function TO_TASK(X: TASK_ID) return SOME_TASK_TYPE; Then, the user could do this: X: TASK_ID; Y: TASK_ID; function CONVERT is new TO_TASK(MY_TASK_TYPE); function CONVERT is new TO_TASK(MY_OTHER_TASK_TYPE); ... abort MY_TASK_TYPE'(CONVERT(X)); if MY_OTHER_TASK_TYPE'(CONVERT(Y))'TERMINATED then ... This alternative does not fully solve the problem, because it requires the specific type to be known in order to apply the class-wide operations. Sub-Alternative B: The operations that are currently defined on task types would also be defined for type TASK_ID. Thus, the following two lines would be equivalent: abort MY_TASK; abort MY_TASK'IDENTITY; Using the abort statement on a TASK_ID would normally be used only if the specific type of the task is not known at compile time. Sub-Alternative C: For each of the operations that are currently defined on task types, we would define a subprogram in the Real Time Annex. For example, the Annex might define a package: package MISCELLANEOUS_TASK_OPERATIONS is procedure DO_ABORT(T: TASK_ID); function IS_CALLABLE(T: TASK_ID) return BOOLEAN; function IS_TERMINATED(T: TASK_ID) return BOOLEAN; end MISCELLANEOUS_TASK_OPERATIONS; Then, this: abort T1, T2; would be (almost) equivalent to this: DO_ABORT(T1'IDENTITY); DO_ABORT(T2'IDENTITY); (I said "almost" because in the former case the abort happens in parallel. But it's close enough.) The DO_ABORT procedure would normally be used only if the specific type of the task is not known at compile time. ALTERNATIVE 2: THE HANDLE TYPE IS AN ACCESS TYPE. In this alternative, we give an explicit name to Ada 83's task class: perhaps SYSTEM.TASK_CLASS. The abort statement, for example, is defined, as in Ada 83, to accept any type in the class. The only new thing is that the class has a name. It is then possible to make an assignable handle type by defining an access type. In fact, the current version of the Systems Programming Annex does just that, in the SYSTEM.TASK_IDENTIFICATION package: type TASK_PTR is access constant TASK_CLASS; Heterogenous data structures can be built from this data type. Operations can be defined in terms of either TASK_PTR or TASK_CLASS, as convenient. Either way, the operations work for both: X: MY_TASK_TYPE; Y: TASK_PTR; abort X, Y.all; We don't need the three sub-alternatives of Alternative 1, since the normal matching rules apply. DISCUSSION: One issue is whether one should be allowed to call a task's entries when one has a handle. For example, suppose I have a task handle X, but I happen to know that it is "really" representing a task of type MY_TASK_TYPE, which has an entry E. Is there some way to call entry E? The answers are: Alternative 1, Sub-Alternative A: Yes. I write something like this: CONVERT(X).E; We might want to define the conversion operation to raise an exception if the conversion is invalid, in order to support this capability safely. Alternative 1, Sub-Alternative B: No. The class-wide operations, such as the abort statement, work, but individual entries do not. Alternative 1, Sub-Alternative C: No, as for Sub-Alternative B. Alternative 2: Yes. I write something like this: MY_TASK_TYPE(X).E; A run-time check is done, as suggested above for Alternative 1, Sub-Alternative A. This capability is not essential, but it could be useful. For example, a user-defined scheduler might have a special task that is defined by the author of the schedule, plus an arbitrary number of application-defined tasks. The scheduler might want to call an entry of the special task in order to make it perform whatever its special action is. This is not essential, because the same thing can be achieved using protected objects. Another issue is when task ids are assigned. If they are assigned by the run-time system when a task is created, then they will not be valid during elaboration of the task_declaration. For alternative 2, the answer is clear: the task id is valid as soon as the object declaration is valid. For alternative 1, we could define it either way. Some may object that Alternative 2 violates type checking in some sense. This is not true. Both alternatives are equivalent in this regard -- the programmer gets the same amount of compile time type checking in each case. In both alternatives, there are operations that don't care about the specific type of a class. Alternative 1 has a magic type that can be a handle on an object of any task type. Special operations convert from task object to task handle and back. Alternative 1 is based on the Ada 83 notion of class. Operations ('ACCESS and .all) convert from task object to task handle and back. Both alternatives raise the issue of dangling pointers. In alternative 1, dangling pointers are erroneous, unless we decide to add some run-time checking. A generation count provides probabilistic checking. It is cheap, but it is not guaranteed to work in all cases. That's probably good enough, although it is difficult to know how to specify it in Reference-Manual-ese. Alternative 2 prevents dangling pointers, so long as 'UNCHECKED_ACCESS is not used. If 'UNCHECKED_ACCESS is used, which would be necessary, for example, to support nested tasks in a user-defined scheduler, dangling pointers are erroneous. The MRT has no strong opinion as to which alternative should be chosen. We believe that Alternative 2 is more in the spirit of Ada; e.g. it's strange to set the priority of a task handle, instead of of the task object itself. But this is really a matter of taste.