Different language features are often compared in terms of theirexpressive power andease of use(usability). Expressive power is the more objective criterion, and is concerned with the ability of language features to allow application requirements to be programmed directly. Ease of use is more subjective, and includes the ease with which the features under investigation interact with each other and with other language primitives.
In her evaluation of synchronisation primitives, Bloom (1979) used the follow- ing criteria to evaluate and compare the expressive power and usability of different language models. She identified several issues that need to be addressed when determining the order of interaction between synchronising agents; a list of such issues is as follows:
(a) type of service requested;
(b) order of request arrival;
(c) internal state of the receiver (including the history of its usage);
(d) priority of the caller;
(e) parameters to the request.
It should be clear that the Ada model described so far deals adequately with the first three situations. The fourth will be considered in Chapter 13, when real-time issues are discussed in detail. Here, attention is focused on the fifth issue, which causes some difficulties for avoidance synchronisation mechanisms.
8.1.1 The resource allocation problem
Resource allocation is a fundamental problem in all aspects of concurrent program- ming. Its consideration exercises all Bloom’s criteria and forms an appropriate basis for assessing the synchronisation mechanisms of concurrent languages.
Consider the problem of constructing a resource controller that allocates some resource to a group of client agents. There are a number of instances of the resource but the number is bounded; contention is possible and must be catered for in the design of the program. If the client tasks only require a single instance of the resource, then the problem is straightforward. For example, in the following, the resource (although not directly represented) can be encoded as a protected object:
protected Resource_Controller is entry Allocate(R : out Resource);
procedure Release(R : Resource);
private
Free : Natural := Max;
...
end Resource_Controller;
protected body Resource_Controller is
entry Allocate(R : out Resource) when Free > 0 is begin
Free := Free - 1;
...
end Allocate;
procedure Release(R : Resource) is begin
Free := Free + 1;
...
end Release;
end Resource_Controller;
To generalise this code requires the caller to state how many resources are re- quired (up to some maximum). The semantics required (to help avoid deadlocks) are that either all requested resources are assigned to the caller or none are (and the caller blocks until the resources are free).
8.1 The need for requeue 165 This resource allocation problem is difficult to program with avoidance synchro- nisation. In order to determine the size of the request, the communication must be accepted and the parameter read. But if, having read the parameter, the internal state of the resource controller is such that there are currently not enough resources available, then the communication must be terminated and the client must try again.
To prevent polling, a different entry must be tried. A detailed examination of this problem has shown that an acceptable solution is not available if avoidance syn- chronisation only is used. Note that a solution is possible, so the issue is one of ease of use rather than expressive power. Nevertheless, the elegance of this solution is poor when compared with the monitor solution given in Section 3.8. A monitor uses condition synchronisation (not avoidance synchronisation) and it is therefore trivially easy to block the caller after the parameter has been read but before it leaves the monitor.
Using entry families
A possible solution in Ada (without requeue) to the resource allocation problem assumes that the number of distinct requests is relatively small and can be repre- sented by a family of entries. Each entry in the family is guarded by the boolean expressionF <= Free, whereFis the family index:
type Request_Range is range 1 .. Max;
protected Resource_Controller is
entry Allocate(Request_Range)(R : out Resource);
procedure Release(R : Resource; Amount : Request_Range);
private
Free : Request_Range := Request_Range’Last;
...
end Resource_Controller;
protected body Resource_Controller is
entry Allocate(for F in Request_Range)(R : out Resource) when F <= Free is
begin
Free := Free - F;
...
end Allocate;
procedure Release(R : Resource; Amount : Request_Range) is begin
...
Free := Free + Amount;
end Release;
end Resource_Controller;
Although this solution is concise, there are two potential problems:
(1) This may not be practical for a large number of resources, as there needs to beMaxentry queues; these must be serviced individually, which could be inefficient. Also, for server tasks there is no equivalent syntactical form for families and hence an excessively long select statement must be used.
(2) It is difficult to allocate the resources selectively – when several requests can be serviced, an arbitrary choice between them is made (note that if the Real-Time Systems Annex is supported, the request can be serviced in a priority order; if all calling tasks have the same priority, then the family is serviced from the smallest index to the largest).
The latter problem can be ameliorated by having each entry in the family guarded by its own boolean, and then selectively setting the booleans toTrue. For exam- ple, if on freeing new resources it is required to service the largest request first, the following algorithm can be used. Note that a request toAllocatethat can be satisfied immediately is always accepted. Hence a boolean variable (Normal) is needed to distinguish between a normal allocation and a phase of allocations following aRelease:
type Request_Range is range 1 .. Max;
type Bools is array(Request_Range) of Boolean;
protected Resource_Controller is
entry Allocate(Request_Range)(R : out Resource);
procedure Release(R : Resource; Amount : Request_Range);
private
Free : Request_Range := Request_Range’Last;
Barrier : Bools := (others => False);
Normal : Boolean := True;
end Resource_Controller;
protected body Resource_Controller is
entry Allocate(for F in Request_Range)(R : out Resource) when F <= Free and (Normal or Barrier(F)) is begin
Free := Free - F;
if not Normal then Barrier(F) := False;
Normal := True;
for I in reverse 1 .. F loop
if Allocate(I)’Count /= 0 and I <= Free then Barrier(I) := True;
Normal := False;
exit;
end if;
end loop;
end if;
...
end Allocate;
8.1 The need for requeue 167 procedure Release(R : Resource;
Amount : Request_Range) is begin
Free := Free + Amount;
for I in reverse 1 .. Free loop if Allocate(I)’Count /= 0 then
Barrier(I) := True;
Normal := False;
exit;
end if;
end loop;
...
end Release;
end Resource_Controller;
Note that the loop bound in entryAllocateisFnotFreeas there cannot be a task queued onAllocaterequiring more thanFinstances of the resource.
The correctness of this algorithm relies on the property that requests already queued upon Allocate (when resources are released) are serviced before any new call toAllocate(from outside the resources controller). It also relies on tasks not removing themselves from entry queues, that is, after the count attribute has been read (and the associated barrier raised) but before the released task ac- tually executesAllocate. Furthermore, it assumes that tasks only ever release resources that they have acquired.
The double interaction solution
One possible solution to the resource allocation problem, which does not rely on a family of entries, is for the resource controller to reject calls that cannot be satisfied.
In this approach, the client must first request resources and, if refused, try again.
To avoid continuously requesting resources when no new resources are available, the client calls a different entry from the original request entry:
type Request_Range is range 1 .. Max;
protected Resource_Controller is
entry Allocate(R : out Resource; Amount : Request_Range;
Ok : out Boolean);
entry Try_Again(R : out Resource; Amount : Request_Range;
Ok : out Boolean);
procedure Release(R : Resource; Amount : Request_Range);
private
Free : Request_Range := Request_Range’Last;
New_Resources_Released : Boolean := False;
...
end Resource_Controller;
protected body Resource_Controller is
entry Allocate(R : out Resource; Amount : Request_Range;
Ok : out Boolean) when Free > 0 is begin
if Amount <= Free then Free := Free - Amount;
Ok := True;
-- allocate else
Ok := False;
end if;
end Allocate;
entry Try_Again(R : out Resource; Amount : Request_Range;
Ok : out Boolean) when New_Resources_Released is begin
if Try_Again’Count = 0 then
New_Resources_Released := False;
end if;
if Amount <= Free then Free := Free - Amount;
Ok := True;
-- allocate else
Ok := False;
end if;
end Try_Again;
procedure Release(R : Resource; Amount : Request_Range) is begin
Free := Free + Amount;
-- free resources
if Try_Again’Count > 0 then New_Resources_Released := True;
end if;
end Release;
end Resource_Controller;
To use this controller, each client must then make the following calls:
Resource_Controller.Allocate(Res,N,Done);
while not Done loop
Resource_Controller.Try_Again(Res,N,Done);
end loop;
Even this code is not entirely satisfactory, for the following reasons:
(1) The clients mustTry Againfor their resources each time any resources are released; this is inefficient.
(2) If a client is tardy in calling Try Again, it may miss the opportunity to acquire its resources (as only those tasks queued on Try Again, at the point when new resources become available, are considered).
8.1 The need for requeue 169 (3) It is difficult to allocate the resources selectively – when several requests
can be serviced, then they are serviced in a FIFO order.
An alternative approach is to require the resource controller to record outstanding requests:
type Request_Range is range 1 .. Max;
protected Resource_Controller is
entry Allocate(R : out Resource; Amount : Request_Range;
Ok : out Boolean);
entry Try_Again(R : out Resource; Amount : Request_Range;
Ok : out Boolean);
procedure Release(R : Resource; Amount : Request_Range);
private
Free : Request_Range := Request_Range’Last;
New_Resources_Released : Boolean := False;
...
end Resource_Controller;
protected body Resource_Controller is
procedure Log_Request(Amount : Request_Range) is begin
-- store details of request end Log_Request;
procedure Done_Request(Amount : Request_Range) is begin
-- remove details of request end Done_Request;
function Outstanding_Requests return Boolean is begin
-- returns True if there are outstanding requests to -- be serviced
end Outstanding_Requests;
procedure Seen_Request(Amount : Request_Range) is begin
-- log details of failed request end Seen_Request;
function More_Outstanding_Requests return Boolean is begin
-- returns True if there are outstanding requests -- to be serviced which have not been considered -- this time around
end More_Outstanding_Requests;
entry Allocate(R : out Resource; Amount : Request_Range;
Ok : out Boolean)
when Free > 0 and not New_Resources_Released is begin
if Amount <= Free then Free := Free - Amount;
Ok := True;
-- allocate else
Ok := False;
Log_Request(Amount);
end if;
end Allocate;
entry Try_Again(R : out Resource; Amount : Request_Range;
Ok : out Boolean) when New_Resources_Released is begin
if Amount <= Free then Free := Free - Amount;
Ok := True;
Done_Request(Amount);
-- allocate else
Ok := False;
Seen_Request(Amount);
end if;
if not More_Outstanding_Requests then New_Resources_Released := False;
end if;
end Try_Again;
procedure Release(R : Resource; Amount : Request_Range) is begin
Free := Free + Amount;
-- free resources
if Outstanding_Requests then New_Resources_Released := True;
end if;
end Release;
end Resource_Controller;
In order to ensure that tasks waiting on theTry Againentry are serviced before new requests, it is necessary to guard theAllocate entry. Unfortunately, this algorithm then breaks down if the client does not make the call to Try Again (due, for example, to being aborted or suffering an asynchronous transfer of control – see Section 9.3). To solve this problem, it is necessary to encapsulate the double interaction in a procedure and provide a dummy controlled variable (as was done in Subsection 6.6.1) which, during finalisations, informs the resource controller (via a new protected procedureDone Waiting) that it is no longer interested:
type Resource_Recovery is new
Finalization.Limited_Controlled with null record;
procedure Finalize(Rr : in out Resource_Recovery) is begin
8.1 The need for requeue 171 Resource_Controller.Done_Waiting;
end Finalize;
procedure Allocate(R : out Resource; Amount : Request_Range) is Got : Boolean;
Protection : Resource_Recovery;
begin
Resource_Controller.Allocate(R, Amount, Got);
while not Got loop
Resource_Controller.Try_Again(R, Amount, Got);
end loop;
end Allocate;
Note that with this solution, the Done Waitingroutine will be called every timethe procedureAllocateis left (either normally or because of task abortion).
The resource controller will therefore have to keep track of the actual client tasks rather than just the requests. It can do this by using task identifiers provided by the Systems Programming Annex. The controller can then determine if a task executingDone Waitinghas an outstanding request.
Even with this solution, the controller still has difficulty in allocating resources selectively. However, the fundamental problem with this approach is that the task must make a double interaction with the resource controller even though only a single logical action is being undertaken.
8.1.2 Solutions using language support
Two methods have been proposed to increase the effectiveness of avoidance syn- chronisation. One of these, requeue, has been incorporated into Ada. The other approach, which is less general purpose, is to allow the guard/barrier to have ac- cess to ‘in’ parameters. This approach is adopted in the language SR. The resource control problem is easily coded with this approach; for example, using Ada-like syntax:
type Request_Range is range 1..Max;
protected Resource_Controller is
entry Allocate(R : out Resource; Amount : Request_Range);
procedure Release(R : Resource; Amount : Request_Range);
private
Free : Request_Range := Request_Range’Last;
...
end Resource_Controller;
protected body Resource_Controller is
entry Allocate(R : out Resource; Amount : Request_Range) when Amount <= Free is -- Not Legal Ada
begin
Free := Free - Amount;
end Allocate;
procedure Release(R : Resource; Amount : Request_Range) is begin
Free := Free + Amount;
end Release;
end Resource_Controller;
The main drawback with this approach is implementational efficiency. It is no longer possible to evaluate a barrier once per entry; each task’s placement on the entry queue will lead to a barrier evaluation. However, optimisations are possible that would allow a compiler to recognise when ‘in’ parameters were not being used; efficient code could then be produced.
The second solution to this problem is to provide a requeue facility.
Important note:
The key notion behind requeue is to move the task (which has been through one guard or barrier – we shall use the term ‘guard’ in this discussion) to ‘beyond’ another guard.
For an analogy, consider a person (task) waiting to enter a room (protected ob- ject) which has one or more doors (guarded entries) giving access to the room.
Once inside, the person can be ejected (requeued) from the room and once again be placed behind a (potentially closed) door.
Important note:
Ada allows requeues between task entries and protected object en- tries. A requeue can be to the same entry, to another entry in the same unit, or to another unit altogether. Requeues from task entries to protected object entries (and vice versa) are allowed. However, the main use of requeue is to send the calling task to a different entry of the same unit from which the requeue was executed.
The resource control problem provides illustrative examples of the application of requeue. One solution is given now; some variations are considered later in this chapter (Section 8.4).
Requeue example – concurrent solution to the resource control problem
One of the problems with the double interaction solution was that a task could be delayed (say, due to preemption) before it could requeue on theTry Againentry.
Consequently, when new resources became available it was not in a position to have them allocated. Requeue allows a task to be ejected from a protected object and placed back on an entry queue as an atomic operation. It is therefore not possible for the task to miss the newly available resources.
In the following algorithm, an unsuccessful request is now requeued on to a
8.1 The need for requeue 173 private entry (calledAssign) of the protected object. The caller of this protected object now makes a single call onAllocate. Whenever resources are released, a note is taken of how many tasks are on theAssignentry. This number of tasks can then retry to either obtain their allocations or be requeued back onto the same Assignentry. The last task to retry closes the barrier:
type Request_Range is range 1 .. Max;
protected Resource_Controller is
entry Allocate(R : out Resource; Amount : Request_Range);
procedure Release(R : Resource; Amount : Request_Range);
private
entry Assign(R : out Resource; Amount : Request_Range);
Free : Request_Range := Request_Range’Last;
New_Resources_Released : Boolean := False;
To_Try : Natural := 0;
...
end Resource_Controller;
protected body Resource_Controller is
entry Allocate(R : out Resource; Amount : Request_Range) when Free > 0 is
begin
if Amount <= Free then Free := Free - Amount;
-- allocate else
requeue Assign;
end if;
end Allocate;
entry Assign(R : out Resource; Amount : Request_Range) when New_Resources_Released is
begin
To_Try := To_Try - 1;
if To_Try = 0 then
New_Resources_Released := False;
end if;
if Amount <= Free then Free := Free - Amount;
-- allocate else
requeue Assign;
end if;
end Assign;
procedure Release(R : Resource; Amount : Request_Range) is begin
Free := Free + Amount;
-- free resources
if Assign’Count > 0 then
To_Try := Assign’Count;
New_Resources_Released := True;
end if;
end Release;
end Resource_Controller;
Note that this will only work if theAssignentry queuing discipline is FIFO.
When priorities are used, two entry queues are needed. Tasks must requeue from one entry to the other (and back again after the next release). This is illustrated in the example given in Section 8.4.
Finally, it should be observed that a more efficient algorithm can be derived if the protected object records the smallest outstanding request. The barrier should then only be set to true in Release(or remain true in Assign) ifFree >=
Smallest.
Even with this style of solution, it is difficult to give priority to certain requests other than in FIFO or task priority order. As indicated earlier, to program this level of control requires a family of entries. However, with requeue, a more straight- forward solution can be given (as compared with the earlier code that did not use requeue):
type Request_Range is range 1 .. Max;
type Bools is array(Request_Range) of Boolean;
protected Resource_Controller is
entry Allocate(Request_Range)(R : out Resource);
procedure Release(R : Resource; Amount : Request_Range);
private
entry Assign(Request_Range)(R : out Resource);
Free : Request_Range := Request_Range’Last;
Barrier : Bools := (others => False);
...
end Resource_Controller;
protected body Resource_Controller is
entry Allocate(for F in Request_Range)(R : out Resource) when True is
begin
if F <= Free then Free := Free - F;
...
else
requeue Assign(F);
end if;
end Allocate;
entry Assign(for F in Request_Range)(R : out Resource) when Barrier(F) is
begin