THE SIMPLE RENDEZVOUS
COMMUNICATION BETWEEN TASKS
In the last chapter, we ran a few programs with tasking, but they all used simple tasking with no form of synchronization. In a real situation using tasking, some form of communication between tasks will be required so we will study the simple rendezvous in this chapter.
Example program ------> e_c27_p1.ada
Examine the program named e_c27_p1.ada which will illustrate the simple rendezvous. This program consists of two tasks, the one in lines 7 through 22 and the main program itself.
THE entry STATEMENT
The task specification is a little more complicated here than it was in any of the example programs in the previous chapter because we have an entry point declared in the task. The reserved word entry begins the entry declaration and is followed by the entry name, which is Make_A_Hot_Dog in this case. The entry statement defines the interface to the outside of a task in much the same way as the procedure header defines the external interface to a package in the package specification. Unlike a package, no types, variables, or constants are allowed to be declared in the task specification, but there is no limit to the number of entry points allowed. An entry can have formal parameters declared as part of the entry name, (we will have an example in the next example program), but it cannot have a return parameter similar to a function. One or more of the formal parameters is permitted to have a parameter of mode out or in out however, so data can be passed both ways while a synchronization is effected. The in mode is also permitted. We will have more to say about this topic later in this chapter.
THE accept STATEMENT
The task body is a very simple sequence of statements in which a message is output to the monitor, a loop is executed four times, and another message is output. The thing that is new here is the accept statement within the loop. The accept statement begins with the reserved word accept and has the following general form;
accept <entry-name> do <executable statements> end <entry-name>;and any legal Ada statements can be contained within it. When execution of the task reaches the accept statement, the task "goes to sleep" until some other task makes a call to this particular accept statement. The term "goes to sleep" means that the task does not simply sit there and execute a do nothing loop while it is waiting, effectively wasting the resources of the system. Instead, it is actually doing nothing until it is awakened by an entry call. It is also possible to have an accept statement with no statements contained within it. It will have the following form;
accept <entry-name>;This statement will only be used for task synchronization since there is no data passed to the entered task.
THE ENTRY CALL
The main part of the program is another loop with four iterations followed by a statement to display a line of text on the monitor. The only thing unusual about the loop is the statement in line 26 which is an entry call to the entry named Make_A_Hot_Dog in the task named Gourmet. Keep in mind that these are two tasks that are operating in parallel and we will carefully explain what is happening.
The entry call is executed at line 26 which wakes up the sleeping task at line 15, and the task named Gourmet continues from where it went to sleep. Notice that the calling program is not controlling the execution of Gourmet, but Gourmet itself is in control now that it has been allowed to continue operation. The entry call is not like a procedure call where the sequence of operation continues in the procedure, but is instead only a synchronization call that allows Gourmet to continue what it was doing prior to being put to sleep.
WHAT IS THE CALLING PROGRAM DOING NOW?
During the time that the called task is executing statements within its accept block, the calling task is effectively put to sleep, and must wait until the called task completes its accept block. When Gourmet reaches line 19, both tasks are allowed to operate in parallel again until one or both reach their point of rendezvous again. If the main program reaches its entry call before Gourmet is ready to accept the call, then the main program will wait until Gourmet is ready. The accept statement, and the corresponding entry call, are therefore used to synchronize the two tasks.
If you were to move the end of Make_A_Hot_Dog to the line immediately after the accept statement, including a null of course, the output statements would then be running in parallel. The delays have been selected in such a way that after making this change, the hot dog would be eaten before it was made. This is one of the programming exercises at the end of this chapter.
A COUPLE OF FINE POINTS MUST BE MADE HERE
Although the entry call looks very much like a procedure call, it is different because it is not legal to use a use clause for a task. It is required therefore that every entry call must use the extended naming convention, or the "dotted" notation. Renaming can be used to reduce the size of the names used and will be illustrated in the next program.
You will notice that the two tasks were composed of exactly four calls and four executions of the accept statement. This was done on purpose in order to simplify the problem of task termination at this point in our study. The study of task termination will be covered in detail in the next chapter. Until then, you should see what happens when the two loops are different. Change the loop in the Gourmet task to 5 iterations and see what happens if it waits to accept a call that never comes, then make the loop in the main program bigger to see what happens when there is nothing to accept its call. We will study tasking exceptions in a later chapter of this tutorial.
The execution of a return statement (not illustrated here), within an accept statement corresponds to reaching the final end and effectively terminates the rendezvous.
SERVERS AND USERS
Tasks are categorized into two rather loosely defined groups, server tasks and user tasks. A server task is one that accepts calls from other tasks, and a user task is one that makes calls to one or more server tasks. In the present example program, Gourmet is a server task, and the main program is a user task. Some tasks both accept calls and make calls, and they are referred to as intermediate tasks. This terminology, or some other fairly self defining terminology may be used in the literature about Ada, so you should be aware that such classifications may exist.
Be sure to compile and execute this program making the changes suggested earlier and observe the results.
MULTIPLE ENTRY POINTS
Example program ------> e_c27_p2.ada
Examine the program named e_c27_p2.ada for a few added tasking topics beginning with multiple accept statements. You will notice that there are three accept statements in the task body corresponding to a single entry point declared in the task specification. There is no limit to the number of accept statements and they are executed when the logic causes execution to arrive at each one in turn. Remember that the task is executed in the logical order as defined in the sequence of statements, except that each time the logic causes execution to arrive at an accept statement, the task will "go to sleep" until an entry call is made to the waiting accept statement. Moreover, the logic does not care where the entry call comes from, nor does it know where it came from, it only cares that the entry call is made. Thus it is possible for several different tasks to be calling this entry point and allowing the task to progress through its logic. Because there is only one other task in this program, we know exactly where the entry call is being generated.
Careful study of the logic will reveal that the accept statement in line 18 must be called, followed by four calls to line 26, and another call to line 36. After six calls to this entry point, the task will reach the end of its execution and will be completed. You will notice that we make exactly six calls to this entry point by the task which is the main program, so everything will come out right.
RENAMING A TASK ENTRY
The alternate name, which is declared in line 11, is used in the main program task to illustrate the use of the renaming facility. Note that the dotted notation is not required in this case. Once again, you can change either the number of calls or the number of accepts to see the exception error or a deadlock condition.
You should have noticed by now that each of the accept statements has a formal parameter associated with it in the same manner that a subprogram can have. In fact, it is permissible to have as many parameters as you desire to transfer data either from the calling task to the called task or back the other way. All three modes of parameter passing are legal for use and the in mode will be used if none is specified as is done here. There is one difference in a task however, you are not permitted to have a return parameter like a function has, but you can return a value by using an out or an in out parameter.
Since you can pass parameters both ways, the task appears to be executed just like a procedure, but there is a very definite difference as mentioned earlier. A procedure is actually executed by the calling program, but the task is simply let free to run on its own in parallel with the calling program. The task is not a subservient program but is running its own logic as it is coded to do.
Be sure to compile and execute this program after you understand what it is supposed to do.
MULTIPLE CALLING TASKS
Example program ------> e_c27_p3.ada
Examine the program named e_c27_p3.ada for an example of two tasks calling a third task's entry point. The task named Gourmet has a single entry point, but this time there are two formal parameters declared in the task specification and the same two declared in the accept statement. These must agree of course. The entry point named Make_A_Hot_Dog in line 16 contains some text to display and a delay of 0.8 second, because it takes a little time to make a hot dog. You will notice that sometimes mustard is added, and sometimes it is not, depending on who is eating the result. We will say more about this later.
We have two additional tasks in lines 35 through 53 named Bill and John, each of which executes in parallel, and each of which requests a number of hot dogs and consumes them in zero time, since there is no delay prior to their next request. You will also notice that once again the numbers come out right because Bill requests three hot dogs and John asks for two while the task that supplies them makes exactly five. We will discuss better methods of task termination later. For the time being, simply accept the utopian situation depicted here.
ENTRIES STACK UP AT THE ENTRY POINT
Considering all that we have said so far about tasking, you should understand that all three of these tasks begin execution at the same time and Bill and John both request a hot dog immediately. Since there is only one entry point, only one can be served at a time, and the Ada definition does not specify which will be serviced first. It does specify that both will ultimately be served, so there is an implicit queue at the entry point where requests can be stored until they can be serviced in turn. All requests are serviced on a First In First Out (FIFO) basis with no concern for priority of tasks (to be defined later). This queue is a "hidden" queue as far as you, the programmer, are concerned because you have no access to it. You cannot therefore, sort through the entries and redefine the order of execution of the various entry requests. The attribute named COUNT is available which will return the number of requests pending on any entry queue. Its use will be illustrated in the program named e_c29_p6.ada in chapter 29 of this tutorial.
After we study some of the advanced tasking topics, you will have the ability to define several queues with different priorities and set up your own priorities as needed.
THE ORDER OF OUTPUT IS SORT OF ODD
Back to the program at hand. Examining the result of execution will reveal that for some unknown reason, the task named John made the first request and the task named Gourmet made a hot dog with mustard first, because John's task was the one requesting a hot dog with mustard. However, before John was allowed to continue execution, the Gourmet task continued and serviced the next call in its entry queue for Make_A_Hot_Dog, and made a hot dog without mustard for Bill. At this point Gourmet had completed all of its entry calls and allowed one of the other tasks to run. The task named John was then allowed to continue execution. John ate his hot dog and requested another. Once again, by some undefined method, the task named Gourmet was allowed to run and make a second hot dog for John, with mustard, when Bill still hasn't been allowed to eat his first one. This continues until all conditions have been satisfied, which means that five hot dogs have been made, and all five have been consumed. Both consuming tasks declare their lack of hunger, and the task named Gourmet declares that it is out of hot dogs. The program has finally run to completion.
WHAT ABOUT THE FUNNY ORDER OF RESULTS?
Even though the results seemed to come out in a funny order, they did follow all the rules we set down for them to follow. Remember that as an experienced programmer, you are accustomed to seeing everything come out in a very well defined precise order, because you have spent your programming career writing sequential programs. If you look at this output from the point of view of each task, you will see that the output from each task is perfectly sequential, as defined by the logic of the task. Additionally, you will see that the order of execution has been preserved as defined by the various rendezvous, because nobody eats a hot dog before it is made, and there are no hot dogs made too early. The synchronization of the tasks has been done exactly as we requested.
Spend enough time studying the logic here to completely understand what is happening, then compile and execute this program to see if your compiler does anything in a different order.
THE select STATEMENT
Example program ------> e_c27_p4.ada
Examine the program named e_c27_p4.ada for our first example program using a select statement. The select statement is used to allow a task to select between two or more alternative entry points. In effect, a task can be waiting at two or more entry points for an entry call to be made, and can act on the first occurring entry call. The structure of the select statement is given by;
select accept ...; -- Complete entry point logic or accept ...; -- Complete entry point logic or accept ...; -- Complete entry point logic end select;and is illustrated in lines 23 through 33 of the present example program. In this case there are two select alternatives, but there is no limit to the number of selections that can be included. Each additional branch is delimited by the reserved word or. When program control of the task arrives at the select statement in line 23, either entry call can be accepted and acted upon immediately.
THE OVERALL PROGRAM
Common sense tells us that we cannot deliver a hot dog until we stock the shelf with a hot dog, so the program has been written to reflect this. The task requires an entry call to Stock_With_A_Hot_Dog before it begins the loop with the select in it to assure that at least one hot dog will be available. After that, it doesn't care what the order of entry calls is because the select statement in the loop will allow them to occur in any order. This is a very simplistic approach to setting up a precedence requirement in an Ada task, but it is too simple to really be effective which we shall see when we examine some of the problems that can occur.
In the first place, if the main program, or task, fails to call Stock_With_A_Hot_Dog first, the system will simply lock up with the calling program demanding a delivered hot dog and steadfastly refusing to continue until it does, and the called task refusing to deliver one until it has been stocked with one in line 16. The system is in deadlock with both tasks refusing to do anything. You can simulate this condition by reversing the two calls in lines 39 and 40. Your compiler will probably give a message indicating deadlock has occurred and terminate operation of the program. Another problem has to do with the inflexibility of this program, since we have once again counted the number of calls required to complete the two tasks and programmed compatible numbers in the two tasks.
A further problem involves the fact that, after one hot dog has been stocked, there is nothing to prevent us from taking delivery of hundreds of hot dogs without adding any more to the shelves.
When you think you understand this program, compile and execute it, then we will go on to the next program where we will solve two of the three problems mentioned in connection with the present program.
SELECT STATEMENTS WITH GUARDS
Example program ------> e_c27_p5.ada
Examine the program named e_c27_p5.ada for an example of a select statement with guards. The guards are used to guard the entry points of the select statement to prevent the kinds of silly things that happened in the last program. The task body Retail_Hot_Dogs has been modified in this program to include guards in lines 26 and 34 for the select statement in lines 25 through 39. A guard is simply a BOOLEAN condition that must be satisfied before that particular entry point can be accepted and its logic executed. The general form of the select statement with guards is given as;
select when <BOOLEAN condition> => accept ...; -- Complete entry point logic or when <BOOLEAN condition> => accept ...; -- Complete entry point logic or when <BOOLEAN condition> => accept ...; -- Complete entry point logic end select;and there is no limit to the number of permissible selections, each being separated by the reserved word or. In fact, one or more of the selections can have no guard, in which case it is similar to having a guard which always evaluates to TRUE. When the select statement is encountered, each of the guards is evaluated for TRUE or FALSE, and those conditions that evaluate to TRUE are allowed to enter into the active wait state for an entry, while those that have guards evaluating to FALSE are not. Those with guards evaluating to FALSE are treated as if they didn't exist for this pass through the loop. Once the guards are evaluated upon entering the select statement, they are not reevaluated until the next time the select statement is encountered, but remain static.
LIMITING THE NUMBER OF HOT DOGS ON THE SHELF
In this program, when the select statement is entered in line 25, the guard at line 26 is evaluated and if the number of hot dogs on the shelf is less than 8, then the accept statement in line 27 is enabled and we are permitted to stock the shelf with one more hot dog. If the number of hot dogs is greater than zero, as the guard at line 34 tests for us, then the accept statement in line 35 is enabled and we are allowed to deliver a hot dog. Even though we may be allowed to either stock the shelf with a hot dog, or deliver a hot dog, we must wait until some other task requests us to do so before we actually do one of the operations. It should be clear to you that in this particular case we will always be permitted to do at least one of the operations, and in many cases both will be permitted. If none of the guards evaluate to TRUE, then none of the selections can be taken, and the program is therefore effectively deadlocked and the exception named Tasking_Error will be raised. You, the programmer, can trap this exception in much the same way that you can trap any other exception and handle it in your own manner, but the rules are a little different for tasking exceptions than for exceptions raised during sequential operation. We will cover tasking exceptions in detail later.
It should be pointed out that, even if a guard evaluates to FALSE, entries can be added to the entry queue and serviced during subsequent executions of the select statement when the guard may become TRUE. Because of this method of defining the entry queue, no calls to the entry are lost, and the operation is predictable.
This program contains four tasks, counting the main program, with one named Five_Dogs stocking the shelf very quickly with five hot dogs, because of the short delay, and another removing five hot dogs a little slower. The main program stocks and retrieves four hot dogs rather slowly due to the relatively long time delay built into the loop.
WATCH THE GUARDS DO THEIR JOB
When you run this program you will see very little action with the guards because of the selection of the guard limits. The five hot dogs are put on the shelf very quickly, but the upper limit of 8 is never reached, and there are always hot dogs on the shelf to supply the limited demands. In fact, as listed in the result of execution, there are never more than 5 on the shelf, and always more than zero. You should compile and execute the program to see if your compiler does the same thing as the one used for this execution.
Change line 26 so that the limit is 3, and recompile and execute the resulting program. In this case, you will very clearly see that the first guard prevents more than three hot dogs from being placed on the shelf. In effect it builds up the entry queue for the Stock_With_A_Hot_Dog entry point and requires the suppliers to wait for shelf space.
Reverse the delays in lines 46 and 54, as your next exercise, so that the hot dogs are consumed much faster than they are stocked so that the guard on the entry point named Deliver_A_Hot_Dog will be needed to protect the delivery of too many hot dogs. In this case, the queue to this entry point will build up a list of requests to be satisfied as hot dogs are delivered.
THIS PROGRAM IS MUCH BETTER
This program solved two of the three problems listed concerning the last program but we still must use the method of counting the required entry calls and providing the proper number of entries. As promised before, this problem will be remedied soon. Be sure you compile and execute this program three times, once unmodified, and twice with the suggested changes, then study the output to assure yourself that you understand it completely.
A PROTECTED AREA OF MEMORY
Example program ------> e_c27_p6.ada
The example program named e_c27_p6.ada gives a very simple example of data protection. If you have several tasks writing to the same record, it is conceivable that before one is finished writing, another task gets control and begins writing to the same record. This would result in corrupted data and is a major problem when writing programs with multiple tasks. The protected area in lines 8 through 39 defines three procedures that operate just like any other procedures we have worked with in this tutorial, with the exception that they are protected from multiple entry. It is impossible for more than one task to be executing code within these three procedures at once because that is their purpose. If more than one task requests a call into these three procedures, one will enter the procedure it called, and the others will be made to wait outside of the procedure they called until the first one leaves the procedure it called. This prevents data corruption.
The remainder of the program is trivial, being composed of three tasks plus the main program that work together to simply add and subtract animals from the common pool. Note that all data must be in the private part of the specification, none is allowed in the protected body of the protected code.
FUNCTIONS ARE A LITTLE DIFFERENT
The procedures are allowed to read or write to the private data, but a function is only allowed to have in mode parameters and it is only allowed to read the data in the private section. Since they are only allowed to read data, multiple tasks are permitted to read the private data at the same time, but not while another task is executing code in a procedure, since it may be writing to the internal data. This permits multiple calls to read which solves the classic problem of readers and writers.
The protected section has a lot more flexibility than we are covering here, but this will give you a good start in understanding what it is used for.
ONE MORE RESERVED WORD
The reserved word requeue is used within a protected block to call another subprogram on behalf of the calling program. The conditions may not yet exist for execution of the desired code, so it is requeue'd until the conditions are right. This is a very advanced technique that you can study on your own when you need it. It will probably be a long time before you reach the level of expertise needed to effectively use this technique.
Return to the Table of Contents