SIMPLE TASKING
WHAT IS TASKING?
The topic of tasking is probably new to you regardless of what programming experience you have, because tasking is a relatively new technique which is not available with most programming languages. If you need some kind of a parallel operation with most other languages, you are required to use some rather tricky techniques or write a driver in assembly language. With Ada, however, tasking is designed into the language and is very easy to use.
BUT NOT TRULY PARALLEL IN ADA
Tasking is the ability of the computer to appear to be doing two or more things at once even though it only has one processor. True parallel operation occurs when there actually are multiple processors available in the hardware, but since most modern computers have only one processor, we must make the system appear to have more than one by sharing the processor between two or more tasks. We will have much more to say about this later, but we must discuss another topic first.
REAL-TIME REQUIRES TIMING
Example program ------> e_c26_p1.ada
In the initial design of the Ada programming language, a requirement was included that it be capable of operating in a real-time environment. This requires that we have some control of time. We at least need the ability to read the current time and know when we arrive at some specified time. The example program named e_c26_p1.ada will illustrate how we can do just that.
The program begins in our usual way except for the addition of the new package listed in line 5, the Ada.Calendar package which must be supplied with your compiler if you have a validated Ada compiler. The specification package for Ada.Calendar is listed in section 9.6 of the Ada 95 reference Manual (ARM) and is probably listed somewhere in your compiler documentation. This package gives you the ability to read the system time and date, and allows you to set up a timed delay. Refer to a listing of the specification package of Ada.Calendar and follow along in the discussion in the next paragraph.
THE CLOCK FUNCTION
You will notice that the type TIME is private, so you cannot see how it is implemented, but you won't need to see it. A call to the function Clock returns the current time and date to a variable of type TIME, and other functions are provided to get the individual elements of the date or the number of seconds since midnight. You cannot read the individual elements directly, because some may change between subsequent reads leading to erroneous data. A procedure named Split is provided to split up the type TIME variable and return all four fields at once, and another is provided named Time_Of which will combine the individual elements into a TIME type variable when it is given the four elements as inputs.
Finally, you are provided with several overloadings of the addition, subtraction, and some compare operators in order to effectively use the Ada.Calendar package. A single exception is declared which will be raised if you attempt to use one of these subprograms wrong.
THE delay STATEMENT
The reserved word delay is used to indicate to the computer that you wish to include a delay at some point in the program. The delay is given in seconds as illustrated in line 19 of the program under study, and is declared as a fixed point number, which is defined by each implementation. The value of the delay is of type DAY_DURATION, but in this case, the universal_real type is used. The exact definition of the delay is given in Annex M of your compiler. It must allow a range of at least 0.0 to 86,400.0, which is the number of seconds in a day, and it must allow a delta of not more than 20 milliseconds. Refer to Annex M of your compiler documentation to see the exact declaration for this type for your particular compiler. The ARM requires that when the delay statement is encountered, the system must delay at the point of occurrence for at least the period specified in the delay statement, but does not say how much longer the system can delay. This leads to some inaccuracy in the delay which will be up to you to take care of. We will see how later in this example program.
A fixed point variable is used for the delay variable so that addition of times can be done with no loss in accuracy, and fixed point numbers have a fixed accuracy.
When you execute this program, you will see the first line displayed on the monitor, then a pause before the second message is displayed due to the delay statement in line 19. In fact, the pause will be at least 3.14 seconds according to the Ada specification.
USING THE CLOCK FUNCTION
In line 22, the Clock function is used to return the current time and date and assign it to the variable named Time_And_Date. In the next line we use the procedure named Split to split the time and date, which is contained in the composite variable named Time_And_Date, into its various components. Although the only component we are interested in is the Seconds field, we must provide a variable for each of the other fields simply because of the nature of the procedure call. Within the function call, we assign the value of Seconds to Start for later use. This is a record of the time when we started the loop which we will use later. The time is in the form of the number of seconds that have elapsed since midnight according to the definition of the calendar package.
We execute a loop in lines 25 through 38 where we read the time and date, split it into its component parts, and display each of the components. Instead of displaying the time since midnight, we subtract the Start time from the current time in line 35, where we are actually using one of the overloadings from the Ada.Calendar package. We display the elapsed time since we executed line 22 of this program. Finally, we put in a total delay of one second each time we pass through the loop so we can see the delays accumulate.
WE ARE ACCUMULATING ERRORS IN THIS LOOP
You will recall that the delay statement requires a delay of at least the amount listed, but says nothing of extra delay allowed when returning to the program, in order to give the compiler writers leeway in how to implement the delay statement. In addition, we will require some time to execute the other statements in the loop, so it should not be surprising to you that when you compile and execute this program, the time will not advance by exactly one second for each pass through the loop, but will precess slightly as time passes.
A LOOP WITHOUT ERRORS
In lines 42 through 51, we essentially repeat the loop but with a slight difference. Instead of delaying for one second in each loop, we delay the amount needed to get to the desired point in time. In line 50, we convert the type of Index to DAY_DURATION with an explicit type conversion, then subtract the current elapsed time to calculate the desired time of the delay. This prevents an accumulation of error, and when you run the program you will see only the digitizing error introduced by the fixed point number. However, there is a potential problem with this method.
WHAT IF A NEGATIVE DELAY IS REQUESTED?
When calculating delay times like this, it is possible for the required delay time to result in a negative number. The Ada designers had enough foresight to see that in most applications, you would desire to simply push forward, so they defined the delay such that a negative value for the delay time would be construed as a zero, and no error would be raised. If a negative time should be considered an error condition for your application, it is up to you to detect it and issue an appropriate error message, or raise an exception.
The final point to be made about this example program is the delay until statement illustrated in line 56. The system delays here until the absolute time given which is of type Time. If the time given is previous to the time when this statement is executed, there will be no delay. The rest of this loop is trivial, so you can study it on your own.
Be sure to compile and execute this program and observe the output. Due to the delays in the first loop, the data output to the monitor is somewhat irregular and can be seen when the program is executed.
THE delay IS NOT PART OF CALENDAR
One final point must be made before we leave this program. The Ada.Calendar package and the delay statement were both introduced here, and even though they work well together, they are completely separate. The delay statement is not a part of the Ada.Calendar package as will be evidenced in the example programs later in this chapter.
OUR FIRST TASKING EXAMPLE
Example program ------> e_c26_p2.ada
Examine the file named e_c26_p2.ada for an example program containing some tasking. The first thing you should examine is the main program consisting of a single executable statement in line 38 that outputs a line of text to the monitor. It may seem strange that it doesn't call any of the code in the declaration part, but it doesn't have to as we shall see.
An Ada task is composed of a task specification and a task body, the former being illustrated in line 7, and the latter being illustrated in lines 8 through 15. This is the first task and both parts begin with the reserved word task. The structure of a task is very similar to the structure of a subprogram or package. This first example is a very simple task which executes a for loop containing output statements. The end result consists of four lines of text being displayed on the monitor.
THE TASK SPECIFICATION
The task specification can be much more involved than this one, but this is a good first example. The general structure for the task specification is given by;
task <task-name> is <entry-points>; end <task-name>;but if there are no entry points, then only the reserved word task is needed followed by the task name. As with the package, the task specification must come before the task body.
THE TASK BODY
The task body begins with the reserved words task and body, followed by the task name, and the remainder of the structure is identical to that of a procedure. There is a declarative part where types, variables, subprograms, packages, and even other tasks can be declared, followed by the executable part which follows the same rules as those for a main program. In this case, there is no declarative part, only an executable part. When this task is executed, it will output four lines to the monitor and stop. We will say a little more about this task when it gets executed in a few minutes.
TWO MORE TASKS
It should be clear to you that there are two additional tasks in this program, one in lines 17 through 25 and another in lines 27 through 35, each with its own respective task specification and task body. There are actually four tasks, because the main program is itself another task that will run in parallel with the three explicitly declared here. Since it is very critical that you understand the order of execution of the various tasks, we will spend some time defining it in detail.
Ada always uses linear declaration and when it loads any program, it loads things in the order given in the listing. Therefore when it finds the task body beginning in line 8, it elaborates all of its declarations, although there are none in this case, then loads the executable part of the task, but does not begin execution yet. It effectively makes the task wait at the begin in line 9 until the other tasks are ready to begin execution. It does the same for the task named Second_Task, and also for Third_Task. When all declarations are elaborated it arrives at the begin of the main program in line 37. At this point, all four tasks are waiting at their respective begin statements, and all four tasks are permitted to begin execution at the same time. All are of equal priority, but a strange thing happens when they begin execution.
ORDER OF EXECUTION IS UNDEFINED BY ADA
The rules of execution, as defined by the ARM, give no requirements that any form of time slicing be done, nor is it illegal for an implementation to allow starving to occur. Starving is where one task uses all of the available time and the other task or tasks are allowed to starve because they receive no time for operation. In addition, there is no required order of execution concerning which of the four tasks in our example will execute first, because we have not included any form of priority. Because of these rules, we cannot predict exactly what your implementation will do, but can only give the results of executing this program on one particular validated Ada compiler. As indicated by the results of execution listed following the program, this particular compiler allows the task named Third_Task to utilize all computing power until it runs to completion, then Second_Task uses all of the resources, followed by First_Task, and finally the main program runs. Your particular compiler may execute these statements in a different order, but that is still correct. The only requirement is that all 17 lines be output in any order. Of course the order of output is defined within any task according to the rules of sequential operation. For example, pass number 2 of Third_Task must follow pass 1 of the same task.
HOW DOES TASKING END?
When the task named Third_Task arrived at its end statement in line 35, it had executed all of its required statements and had nothing else to do, so it simply waited there until the other tasks were completed. When all four tasks, including the main program task, had arrived at their respective end statements, the Ada system knew there was nothing else to do, so it returned to the operating system as it does at any normal program completion.
Because of the way this program operates, it is not clear that all four tasks are operating in parallel, but they actually are, as we will see in the next example program. Compile and execute this program and study the output from your compiler to see if it is different from that obtained as output from our compiler.
ADDING DELAYS ADDS TO THE CLARITY OF OPERATION
Example program ------> e_c26_p3.ada
Examine the next example program named e_c26_p3.ada and you will notice that delay statements are added within each of the loops. The delays were chosen to illustrate that each loop is actually operating in parallel. The main program is identical to the previous program if you ignore the commented out statements, and since the main program has no delay prior to its output statement, it will be the first to output to the monitor. The main program will also be the first to complete its operation and will then patiently wait at its end statement until the other three tasks complete their jobs.
Because of the differences in the delays, the three tasks will each complete their delays at different times and the output should be very predictable. If you examine the result of execution given at the end of the program, you will see that the three tasks actually are running in parallel as we stated earlier. It would be profitable for you to compile and execute this program so you can observe the output firsthand.
WHAT ABOUT THE MAIN PROGRAM?
Return once again to the program named e_c26_p3.ada so we can examine the main program. You should remove the comment marks from the three statements in the main program so that it also contains a loop and a delay within each pass through the loop. When you compile and execute the program as modified, it will execute a delay prior to the first output, and will output a total of five lines to the monitor interlaced with the outputs of the other tasks. This should be a good indication to you that the main program is acting just like another task. Compile and execute this program, as modified, to see that the main program acts just like another task.
A BLOCK CAN HAVE TASKS WITHIN IT
Example program ------> e_c26_p4.ada
Examine the program named e_c26_p4.ada for an example of a main program with tasks embedded within it. There is a block declared in the main program in lines 11 through 44 which is executed inline just like any other block. This block however, has the same three tasks we have used in the previous programs embedded within it. You will notice that the order of declaration is different here, since the three task specifications are given together in lines 12 through 14, but this is perfectly legal and would be legal in the last two programs also. The only requirement is that the task specification must be given before the task body for each declared task.
WHAT GOOD IS A DELAY OF ZERO?
You will notice that all of the delays are of zero time, which may lead you to ask why we should even bother to include the delays. Each time the system encounters a delay of any kind, it must look at each task to see if any of them have timed out and need to be exercised. When it does that, it may advance to the next task in order, but it is not required to. Which task will be selected as the next task is undefined by the ARM, so any task can be executed next, including the one that is presently executing. The end result is that all four tasks, including the one in the executable part of the block, may be executed in a round robin fashion, and none of the tasks are starved. It would be perfectly legal for a single task to starve the others, according to the ARM, so if your compiler allows this, it is not an error. Tasking, as used in a real programming situation will not have this problem since additional constructs will be included as we will discuss in the next few chapters of this tutorial.
In a manner similar to that in the other example programs, when all four tasks are at their end points, the block is completed, and the remaining statement is executed in the main program. Note carefully that the main program did not enter into the tasking that was executed within the block. The inline block was executed as another sequential statement as part of the main program. Thus line 9 was executed, then the block in lines 11 through 44 was executed. When all four tasks including the one within the block body were completed, the output statement in line 46 was allowed to execute.
Compile and execute this program and study the results. Use the results to prove to yourself that the statements in lines 9 and 46 of the main program did not enter into the tasking.
PROGRAMMING EXERCISES
Return to the Table of Contents