xtc_cskip(3)
---xtc_cskip(3)
RCU-protected concurrent ordered map (skiplist)
| XTC_CSKIP(3) | Library Functions Manual | XTC_CSKIP(3) |
NAME
xtc_cskip_create,
xtc_cskip_destroy,
xtc_cskip_get,
xtc_cskip_insert,
xtc_cskip_remove,
xtc_cskip_min,
xtc_cskip_floor,
xtc_cskip_size —
RCU-protected concurrent ordered map (skiplist)
SYNOPSIS
#include <xtc.h>
#include <xtc_cskip.h>
#include <xtc_rcu.h>
int
xtc_cskip_create(xtc_cskip_cmp_fn
cmp, xtc_cskip_t
**out);
void
xtc_cskip_destroy(xtc_cskip_t
*s);
int
xtc_cskip_get(xtc_cskip_t
*s, const void
*key, void
**out_value);
int
xtc_cskip_insert(xtc_cskip_t
*s, void *key,
void *value,
void
**out_old_value);
int
xtc_cskip_remove(xtc_cskip_t
*s, const void
*key, void
**out_value);
int
xtc_cskip_min(xtc_cskip_t
*s, void **out_key,
void **out_value);
int
xtc_cskip_floor(xtc_cskip_t
*s, const void
*key, void
**out_key, void
**out_value);
size_t
xtc_cskip_size(const
xtc_cskip_t *s);
DESCRIPTION
xtc_cskip is a concurrent ordered map -- a
skiplist (Pugh 1990) built on xtc_rcu(3) so that lookups
are lock-free, walking the tower with acquire-ordered loads and no lock at
all, while writers still support concurrent readers. It is the
ordered
sibling of xtc_chash(3): where the hash table answers only
“is this exact key present”, xtc_cskip
additionally keeps keys in sorted order, so it answers “what is the
smallest key”
(xtc_cskip_min())
and “what is the largest key <= K”
(xtc_cskip_floor()),
the predecessor / range queries an unordered hash table cannot serve.
Keys and values are caller-owned void *
pointers stored verbatim, the same convention as
xtc_chash(3), xtc_chan(3), and
xtc_pdict(3): the map never copies or frees a key or
value. A caller-supplied comparator cmp imposes the
ordering; unlike xtc_chash(3), which only tests equality,
xtc_cskip uses the FULL qsort(3)
ordering (<0, 0, >0), so cmp MUST be a total
order over the key space.
xtc_cskip_create()
allocates an empty map.
xtc_cskip_destroy()
frees the map's own node bookkeeping; it does not free caller-owned
keys/values and is not safe to call concurrently with any other call on the
same map -- quiesce first, exactly as xtc_rcu(3)'s
xtc_rcu_fini()
requires.
xtc_cskip_get(),
xtc_cskip_min(), and
xtc_cskip_floor() must be called inside the caller's
own
xtc_rcu_read_lock()/xtc_rcu_read_unlock()
bracket (nesting is fine). Each writes its result pointer(s) on a hit and
returns XTC_OK, or returns
XTC_E_NOTFOUND on a miss / empty map; the returned
pointers are valid only until the caller's read-side ends.
xtc_cskip_min() reports the entry with the smallest
key; xtc_cskip_floor() reports the entry with the
largest key <= key (and
XTC_E_NOTFOUND if no key is <=
key). Any of the out_* arguments
may be NULL if the caller does not want that half.
xtc_cskip_insert()
and
xtc_cskip_remove()
take the read-side internally; the caller does not need an outer read-side.
xtc_cskip_insert() replaces any existing entry for
an equal key, writing the old value pointer to
*out_old_value (NULL for a fresh insert; the argument
itself may be NULL). xtc_cskip_remove() writes the
removed value pointer to *out_value (may be NULL) and
returns XTC_E_NOTFOUND on a miss.
xtc_cskip_size()
returns the approximate live entry count -- exact with no concurrent
writers.
CONCURRENCY AND RECLAMATION
Readers are lock-free. Writers
(xtc_cskip_insert(),
xtc_cskip_remove()) serialize on a single per-map
mutex in this version -- the reader path is the one that must scale, and it
takes no lock. A finer-grained per-node writer lock is a deliberate deferred
optimization (recorded in the source); it is not needed until a workload
shows writer contention.
An insert links its new node bottom-up, level
0 first: the base level is the source of truth for membership, so the node
is present the instant its level-0 link is published (a single release
store), and the upper levels are only a search accelerator a reader
tolerates seeing half-built. A remove unlinks top-down by overwriting each
predecessor's forward pointer with a release store of the removed node's own
(unchanged) successor, then hands the node to
xtc_rcu_retire()
-- never freed synchronously. A reader already parked on the node keeps
walking its untouched forward pointers until it leaves its read-side; RCU
reclaims the node only after every such reader has drained.
This module never calls
xtc_rcu_synchronize()
itself; as with every other xtc_rcu(3) consumer,
reclamation of retired nodes happens on the caller's, a test's, or an
application-installed reaper's schedule.
RETURN VALUES
XTC_OK on success;
XTC_E_INVAL for a NULL map/key or a missing
comparator at create; XTC_E_NOTFOUND on a
get/remove/min/floor miss; XTC_E_NOMEM on allocation
failure.
EXAMPLES
static int
i64_cmp(const void *a, const void *b)
{
int64_t x = *(const int64_t *)a, y = *(const int64_t *)b;
return x < y ? -1 : (x > y ? 1 : 0);
}
int64_t k1 = 10, k2 = 20, k3 = 30;
xtc_cskip_t *s;
xtc_cskip_create(i64_cmp, &s);
xtc_cskip_insert(s, &k3, "thirty", NULL);
xtc_cskip_insert(s, &k1, "ten", NULL);
xtc_cskip_insert(s, &k2, "twenty", NULL);
xtc_rcu_read_lock();
void *mk, *mv;
if (xtc_cskip_min(s, &mk, &mv) == XTC_OK)
printf("min key %ld -> %s\n", (long)*(int64_t *)mk, (char *)mv);
int64_t q = 25;
void *fk, *fv;
if (xtc_cskip_floor(s, &q, &fk, &fv) == XTC_OK)
printf("floor(25) -> %ld=%s\n", (long)*(int64_t *)fk, (char *)fv);
xtc_rcu_read_unlock();
xtc_cskip_destroy(s);
SEE ALSO
HISTORY
The xtc_cskip ordered map first appeared
in libxtc 1.21.
| July 14, 2026 | Debian |