Formal Verification of Lock-Free Algorithms
Formal Verification of Lock-Free Algorithms
Formal Verification of Lock-Free Algorithms
Create successful ePaper yourself
Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.
Top<br />
a b c<br />
null<br />
Figure 1. Example <strong>of</strong> a Stack representation<br />
CAS(Old,New,G : Pointer){<br />
if G = Old<br />
then {G := New; return TRUE; }<br />
else return FALSE;<br />
}<br />
Figure 2. The atomic CAS operation<br />
1<br />
2<br />
3<br />
4<br />
5<br />
6<br />
7<br />
8<br />
9<br />
10<br />
CPOP(Top : Pointer ) {<br />
repeat<br />
OTop := Top ;<br />
if OTop = null then<br />
return empty;<br />
ONxt := OTop .nxt ;<br />
Res := OTop .val ;<br />
until CAS(OTop , ONxt , Top );<br />
return Res<br />
}<br />
Figure 4. The pop operation<br />
field for the data value and a .nxt pointer to refer to the<br />
succeeding node. The end <strong>of</strong> the stack is marked with the<br />
null pointer. The entry point <strong>of</strong> the stack, i.e. the top cell,<br />
is referenced by the global variable Top.<br />
The push and pop operations consist <strong>of</strong> two phases. First,<br />
the needed data is prepared without changing the main<br />
data structure, e.g. storage is allocated or data are read. In<br />
a second phase, it is attempted to change the stack data<br />
structure in a single atomic step. If this atomic step fails<br />
(e.g. because the data structure has changed), the main data<br />
structure is not changed and the algorithm repeats phase<br />
one. To change the stack atomically, a compare-and-swap<br />
(CAS) command is used. Pseudocode for this operation is<br />
given in Figure 2. The CAS command compares a local<br />
pointer Old with a global pointer G, where Old is typically<br />
a value <strong>of</strong> G, that was read earlier by the process. If both<br />
pointers are identical, G is set to a new value New and<br />
the CAS command succeeds. If the values are different, G<br />
is left unchanged and the CAS command fails. All usual<br />
multi-core processors support atomic CAS instructions (e.g.<br />
CMPXCHG on x86 processors) or equivalent instructions.<br />
The push algorithm is depicted in Figure 3. Parameters <strong>of</strong><br />
CPUSH are the value input value V, which should be placed<br />
on top <strong>of</strong> the stack, and a reference to the Top pointer.<br />
Since each invocation <strong>of</strong> CPUSH runs in parallel with calls<br />
to CPUSH or CPOP from other processes, Top may change<br />
at any time while the algorithm is executed.<br />
Variables UNew and UTop are local pointer variables.<br />
1<br />
2<br />
3<br />
4<br />
5<br />
6<br />
7<br />
8<br />
CPUSH(V : Data, Top : Pointer){<br />
UNew := new(Node);<br />
UNew .val := V;<br />
repeat {<br />
UTop := Top ;<br />
UNew .nxt := UTop ;<br />
} until CAS(UTop , UNew , Top )<br />
}<br />
Figure 3. The push operation<br />
The algorithm starts by allocating a new cell on the heap<br />
and writing the input value V into the newly allocated node<br />
(line 2 and 3). After that the algorithm loops, as long as the<br />
insertion <strong>of</strong> the new cell in the stack fails (line 4 to 7). Inside<br />
the loop the pointer <strong>of</strong> the current top cell is copied to the<br />
local variable UTop and the .nxt-pointer <strong>of</strong> the previously<br />
allocated cell is set to the current top cell (line 5 and 6).<br />
Finally, the algorithms attempts to add the new data value<br />
to the top <strong>of</strong> the stack data structure by a CAS operation<br />
(line 7). If the current Top is still the same as it was in line<br />
5, the previously allocated cell UNew contains the correct<br />
.nxt -pointer and Top can be set to UNew . If the top <strong>of</strong><br />
stack was changed by another push or pop process in the<br />
meantime, the CAS operation fails and the loop is reiterated.<br />
The pop algorithm, shown in Figure 4, is very similar.<br />
The only difference is that it has to be checked, whether the<br />
stack is empty or not (line 4). In case <strong>of</strong> an empty stack, a<br />
special value empty is returned.<br />
3. Linearizability and <strong>Lock</strong>-<strong>Free</strong>dom<br />
The usual correctness criteria for lock-free algorithms are<br />
linearizability for safety and lock-freedom for liveness.<br />
Linearizability was defined by Herlihy & Wing [5]. It<br />
is based on the visible inputs and outputs <strong>of</strong> processes.<br />
<strong>Formal</strong>ly a history is defined as a list <strong>of</strong> invoke and return<br />
events. An invoke event inv(p,op,in) is added to the history,<br />
when process p invokes operation op with input in. Similarly<br />
a return event ret(p,op,out) is added when process p returns<br />
with output out from operation op. In our example a possible<br />
history created by two processes p and q is<br />
H = [inv(p,push,3), inv(q,push,4), ret(p,push,−),<br />
ret(q,push,−), inv(q,pop,−), ret(q,pop,4)]<br />
where the − indicates no input/output. A history is linearizable<br />
if it can be reordered to a sequential history, where each<br />
invoke is directly followed by the corresponding return. The<br />
reordering must respect the order that was present in the<br />
original history: an operation whose invoke happened after<br />
the return <strong>of</strong> another operation, must end up in the same<br />
14<br />
Authorized licensed use limited to: Technische Universitat Kaiserslautern. Downloaded on January 18, 2010 at 11:43 from IEEE Xplore. Restrictions apply.