Ok, so I'm trying to model a CLH-RW lock in Promela.
The way the lock works is simple, really:
The queue consists of a tail
, to which both readers and writers enqueue a node containing a single bool succ_must_wait
they do so by creating a new node and CAS-ing it with the tail
.
The tail thereby becomes the node's predecessor, pred
.
Then they spin-wait on pred.succ_must_wait
until it is false
.
Readers first increment a reader counter ncritR
and then set their own flag to false
, allowing multiple readers at in the critical section at the same time. Releasing a readlock simply means decrementing ncritR
again.
Writers wait until ncritR
reaches zero, then enter the critical section. They do not set their flag to false
until the lock is released.
I'm kind of struggling to model this in promela, though.
My current attempt (see below) tries to make use of arrays, where each node basically consists of a number of array entries.
This fails because let's say A
enqueues itself, then B
enqueues itself. Then the queue will look like this:
S <- A <- B
Where S
is a sentinel node.
The problem now is, that when A
runs to completeness and re-enqueues, the queue will look like
S <- A <- B <- A'
In actual execution, this is absolutely fine because A
and A'
are distinct node objects. And since A.succ_must_wait
will have been set to false
when A
first released the lock, B
will eventually make progress, and therefore A'
will eventually make progress.
What happens in the array-based promela model below, though, is that A
and A'
occupy the same array positions, causing B
to miss the fact that A
has released the lock, thereby creating a deadlock where B
is (wrongly) waiting for A'
instead of A
and A'
is waiting (correctly) for B
.
A possible "solution" to this could be to have A
wait until B
acknowledges the release. But that would not be true to how the lock works.
Another "solution" would be to wait for a CHANGE in pred.succ_must_wait
, where a release would increment succ_must_wait
, rather than reset it to 0
.
But I'm intending to model a version of the lock, where pred
may change (i.e. where a node may be allowed to disregard some of its predecessors), and I'm not entirely convinced something like the increasing version wouldn't cause an issue with this change.
So what's the "smartest" way to model an implicit queue like this in promela?
/* CLH-RW Lock */
/*pid: 0 = init, 1-2 = reader, 3-4 = writer*/
ltl liveness{
([]<> reader[1]@progress_reader)
&& ([]<> reader[2]@progress_reader)
&& ([]<> writer[3]@progress_writer)
&& ([]<> writer[4]@progress_writer)
}
bool initialised = 0;
byte ncritR;
byte ncritW;
byte tail;
bool succ_must_wait[5]
byte pred[5]
init{
assert(_pid == 0);
ncritR = 0;
ncritW = 0;
/*sentinel node*/
tail =0;
pred[0] = 0;
succ_must_wait[0] = 0;
initialised = 1;
}
active [2] proctype reader()
{
assert(_pid >= 1);
(initialised == 1)
do
:: else ->
succ_must_wait[_pid] = 1;
atomic {
pred[_pid] = tail;
tail = _pid;
}
(succ_must_wait[pred[_pid]] == 0)
ncritR++;
succ_must_wait[_pid] = 0;
atomic {
/*freeing previous node for garbage collection*/
pred[_pid] = 0;
}
/*CRITICAL SECTION*/
progress_reader:
assert(ncritR >= 1);
assert(ncritW == 0);
ncritR--;
atomic {
/*necessary to model the fact that the next access creates a new queue node*/
if
:: tail == _pid -> tail = 0;
:: else ->
fi
}
od
}
active [2] proctype writer()
{
assert(_pid >= 1);
(initialised == 1)
do
:: else ->
succ_must_wait[_pid] = 1;
atomic {
pred[_pid] = tail;
tail = _pid;
}
(succ_must_wait[pred[_pid]] == 0)
(ncritR == 0)
atomic {
/*freeing previous node for garbage collection*/
pred[_pid] = 0;
}
ncritW++;
/* CRITICAL SECTION */
progress_writer:
assert(ncritR == 0);
assert(ncritW == 1);
ncritW--;
succ_must_wait[_pid] = 0;
atomic {
/*necessary to model the fact that the next access creates a new queue node*/
if
:: tail == _pid -> tail = 0;
:: else ->
fi
}
od
}