A
Task_Read_Write_Lock
is the most sophisticated type of lock the
Util
subsystem provides. Like the
Read_Write_Lock
, it can be locked
for exclusive (read-write) or shared (read-only) access. It also keeps
track of the tasks that currently hold the lock (its
holders).
A holder may
Acquire
a lock several times and must
Release
it a
corresponding number of times. A holder that holds the lock in shared
mode is called a
reader; an exclusive holder is called a
writer. A
Task_Read_Write_Lock
does handle deserters, i.e.
tasks that terminate while still holding the lock.
A Task_Read_Write_Lock
allows several readers, but only one writer
(and no readers while there is a writer) at any time. It is a
fair lock, i.e. it is starvation-free. (Well, nearly; higher
priority tasks may still keep crowding out lower priority ones, but
that is in the spirit of Ada tasking, and so I consider this to be
acceptable. See the comments in
Util.Tasking.Locks
on the Read_Write_Lock
for more details of the mechanism used.
A Task_Read_Write_Lock
supports lock promotions: a reader may
request exclusive access (by calling Acquire (Exclusive)
)
and may be granted exclusive access and thus become a writer without
relinquishing the lock. A lock promotion is thus atomic.
Lock promotions on read/write locks are a source of either deadlocks or
general confusion. A lock promotion is only possible when the requesting
reader is the only lock holder. If there are two different holders both
requesting a lock promotion, either a deadlock would occur, or lock
promotion wouldn't be atomic anymore and thus not offer any advantages
over simply giving up the read lock and re-acquiring it again in
exclusive mode.
(To see this, consider a case where two reader tasks request lock
promotion to exclusive access on the same lock. They'd both block until
they were the only two readers on the lock, and then one of them would
have its request granted. When it then finished and released the lock,
the other reader would get its promotion request granted, but in the
meantime, the first task has had exclusive (write) access to the
protected resource, and thus the second task couldn't rely on the
resource's state still being the same as it was at the time it issued
its promotion request: the promotion wouldn't be atomic anymore.)
Therefore, there can be only one lock promotion request. A promotion
request blocks the requesting reader until it is the only reader left,
and is then granted, at which moment the reader becomes the writer.
The question now is what to do if a second promotion request arrives
from some other reader, while the first reader's promotion request is
still blocked.
A Task_Read_Write_Lock
gives the user control over this: it uses a
user-defineable conflict resolution policy to decide which of the two
requests shall be aborted by raising a Promotion_Error
exception.
The default policy is to accept only the first request, and to abort
all others, regardless of the task priorities of the two reader tasks.
However, one may install a Promoter
conflict resolver overriding this
default behavior. E.g. to accept the request of the task with the higher
priority, one might use:
type Priority_Conflict_Resolver is
new Conflict_Resolver with null record;
function Decide
(Self : access Priority_Conflict_Resolver;
Queued_Task : in Ada.Task_Identification.Task_Id;
New_Task : in Ada.Task_Identification.Task_Id)
return Boolean
is
use type System.Any_Priority;
begin
return Ada.Dynamic_Priorities.Get_Priority (New_Task) <=
Ada.Dynamic_Priorities.Get_Priority (Queued_Task);
end Decide;
and install an object of this type as the conflict resolution policy of
the lock. In fact, because this particular policy may be used often,
it is provided in package
Conflict_Resolvers
.
Lock demotions also are supported: if the writer task calls
Demote
, it gives up its exclusive access and retains the lock only
in Shared
mode, i.e. it becomes a reader. Again, lock demotion is
atomic: no other waiting writer can acquire the lock. (If there are
waiting readers, these will then acquire the lock in shared mode, too.)
A lock request through Get
or Acquire
is granted if:
- the calling task is not currently a holder:
- if
Mode
= Shared
- if there are only readers.
- if
Mode
= Exclusive
- if there are no readers and no writer.
- the calling task is currently a reader:
- if
Mode
= Shared
- always.
- if
Mode
= Exclusive
- if the calling task is the only reader (lock promotion).
- the calling task is currently the writer:
- if
Mode
= Shared
- always (the lock remains locked for exclusive access).
- if
Mode
= Exclusive
- always.
(But note that all requests are queued, and the queue is processed in
order. So if there's an exclusive or promotion request at the head of
the queue, and that request cannot be granted, requests later in the
queue remain blocked even if they could be granted. Hence jumping the
queue is forbidden to avoid starvation of writers. The only exception
to this is promotion requests: even if the request at the front of the
queue cannot be granted, a promotion request later in the queue will
be granted (if it can). This, too, is necessary because otherwise we'd
deadlock when a writer and a promotion request were on the queue. Yet
this cannot starve a writer, because there can be at most one promotion
request on the queue at any time.)
If a lock holder acquires a lock several times (including lock
promotions), it must also release the lock the same number of times
to relinquish it. Lock demotions don't count, though. Hence
after
My_Lock.Acquire (Exclusive);
My_Lock.Demote;
there needs to be one call to Release
, not two. Basically,
a task needs a Release
for each Acquire
that doesn't raise an
exception, and for each Get
that returns Granted = True
.