[ltt-dev] lock-free data structures (was Re: [PATCH 12/12] centralize definition of BITS_PER_LONG)
Paolo Bonzini
pbonzini at redhat.com
Fri Feb 19 14:25:04 EST 2010
>> Some time we should also add double-long compare-and-swap, that's very
>> useful for lock-free lists.
>
> Do you have pointers to papers describing this double-wide CAS
> linked-list structure ?
I found
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.86.8494&rep=rep1&type=pdf
(page 4) which is a lock-free queue.
The two words are used like this:
struct gen_ptr {
long gen;
void *ptr;
}
where gen is always incremented whenever ptr is changed.
> Yes, good idea! Although this won't be available on all architectures.
> We might have to think of a mutex-based compatibility layer for these.
You don't need a mutex if you use it for a lock-free queue. You're just
better off providing two version of the queue, one for double-word CAS
and a not-really-lock-free one for simple CAS.
The elementary operation in that algorithm are reading a struct gen_ptr
in a way that can be compared-and-swapped later, and doing a
compare-and-swap that checks consistency of generation and pointer and
increments the generation. If you have double-word CAS, everything is
very simple because consistency is taken care of by the double-word CAS.
So with double-word CAS the above are
void *atomicRead (struct gen_ptr *val)
{
return val->ptr;
}
and
long atomicCmpXchgIncrGeneration (struct gen_ptr *val, void *old_ptr,
void *new_ptr)
{
int old_gen = val->gen;
struct gen_ptr read =
atomicCmpXchgDouble (val, old_gen, old_ptr,
old_gen + 1, new_ptr)
return read.gen;
}
but even without double-word CAS you can do it because if the
generations match, so will the pointers. So the CAS will be on the
generation in this case, and you use the low-order bit as a tag bit for
"operation in progress". It would go something like this (untested):
struct gen_ptr atomicRead (struct gen_ptr *val)
{
struct gen_ptr read;
for (;;) {
read.gen = val->gen;
if (read.gen & 1) {
/* Unlucky. */
continue;
} else {
read.ptr = val->ptr;
rmb ();
if (read.gen == val->gen)
return read;
}
}
}
long
atomicCmpXchgWithGeneration (struct gen_ptr *val, long old_gen,
void *new_ptr)
{
assert (!(old_gen & 1));
int read_gen = CAS (&val->gen, old_gen, old_gen + 3);
if (read_gen == old_gen) {
/* We are the only ones that have access to val->ptr here.
Since the low bit is 1, other threads will lock on
atomicRead and fail the CAS in their
atomicCmpXchgWithGeneration. Update val->ptr and untag
the generation number. */
val->ptr = new_ptr;
wmb ();
val->gen--;
}
return read_gen;
}
Now, while the above is certainly fun, I don't think it belongs in
liburcu. However...
> I wonder if we could use a clever RCU structure
> to mimick the double-wide CAS.
I don't know, I think lock-free queues and RCU are used for very
different access patterns (and if you need lock-free, chances are that
lock-free will kill your cache and not give you _that much_ performance,
so maybe you'd better rethink everything...). However I'm not expert at
all.
Still, related to this, the paper above gives only half of the story.
The lock-free queue needs a free list traditionally, and the paper
mentions using a lock-free stack as a free list (BTW the lock-free stack
doesn't need double-word CAS). But then, the nodes on the freelist can
never be returned to the OS because you can always race with the
operations on the lock-free queue.
So, maybe RCU could be used to detect quiescent periods when there is no
operation being performed on the lock-free queue. *Both enqueue and
dequeue* would be wrapped in rcu_read_{,un}lock(), unlike normal RCU
algorithms because this one is lock free. Then, in dequeue you'd
replace the paper's line D19 with
defer_rcu(free, head);
Hmm...
Paolo
More information about the lttng-dev
mailing list