xtc_lrlock(3)

---

xtc_lrlock(3)

left-right lock with wait-free reads

XTC_LRLOCK(3) Library Functions Manual XTC_LRLOCK(3)

xtc_lrlock_create, xtc_lrlock_create_ex, xtc_lrlock_destroy, xtc_lrlock_read_begin, xtc_lrlock_read_end, xtc_lrlock_write_begin, xtc_lrlock_apply_op, xtc_lrlock_publish, xtc_lrlock_publish_full_sync, xtc_lrlock_write_end, xtc_lrlock_read_data, xtc_lrlock_write_data, xtc_lrlock_mark_ready, xtc_alrlock_create, xtc_alrlock_create_exleft-right lock with wait-free reads

#include <xtc.h>
#include <xtc_lrlock.h>

int
xtc_lrlock_create(size_t data_size, xtc_lrlock_apply_fn apply_fn, xtc_lrlock_sync_fn sync_fn, const char *name, xtc_lrlock_t **out);

int
xtc_lrlock_create_ex(const xtc_lrlock_opts_t *opts, xtc_lrlock_t **out);

int
xtc_alrlock_create(size_t data_size, xtc_lrlock_apply_fn apply_fn, xtc_lrlock_sync_fn sync_fn, const char *name, xtc_lrlock_t **out);

int
xtc_alrlock_create_ex(const xtc_lrlock_opts_t *opts, xtc_lrlock_t **out);

void
xtc_lrlock_destroy(xtc_lrlock_t *lr);

const void *
xtc_lrlock_read_begin(xtc_lrlock_t *lr);

void
xtc_lrlock_read_end(xtc_lrlock_t *lr);

void *
xtc_lrlock_write_begin(xtc_lrlock_t *lr);

void
xtc_lrlock_apply_op(xtc_lrlock_t *lr, const void *op, size_t op_size);

void
xtc_lrlock_publish(xtc_lrlock_t *lr);

void
xtc_lrlock_publish_full_sync(xtc_lrlock_t *lr);

void
xtc_lrlock_write_end(xtc_lrlock_t *lr);

const void *
xtc_lrlock_read_data(xtc_lrlock_t *lr);

void *
xtc_lrlock_write_data(xtc_lrlock_t *lr);

void
xtc_lrlock_mark_ready(xtc_lrlock_t *lr);

xtc_lrlock is a wait-free-read concurrency primitive that maintains two copies of the protected state and atomically swaps which copy is current on each writer publish. Readers never block; writers serialise. Ported from the PostgreSQL lrlck branch.

Two copies of the user data live side by side. An atomic read_idx identifies which copy is currently the read copy.

:

  1. Set this thread's bit in the active-readers bitmask.
  2. Bump this thread's epoch counter to odd.
  3. Sequentially-consistent fence.
  4. Atomic-acquire-load the current read_idx.
  5. Read data [read_idx].
  6. Bump epoch back to even. Clear bitmask bit.
No CAS, no spin, no system call.

:

  1. Take writer mutex (one writer at a time).
  2. Mutate the inactive copy via () or by writing to the buffer returned by ().
  3. xtc_lrlock_publish(): swap read_idx, fence, snapshot epoch counters, wait until each previously-active reader has departed, then re-apply the operations to the now-stale copy so both sides are in sync.
  4. () releases the writer mutex.

() opens a lock with default options (64 reader slots, 4 KiB oplog). () takes xtc_lrlock_opts_t explicitly, exposing knobs for max readers, oplog capacity, and flags (notably XTC_LRLOCK_COW) which lazily allocates the second copy and madvise(2) 's it back to the OS after each publish in COW mode .

() and () are exact aliases of the corresponding xtc_lrlock_create() functions, named to make fiber-awareness explicit at the call site. Reads are wait-free and never block, so only the writer's publish waits (for in-flight readers to advance past the version swap); when the writer runs inside a fiber on an xtc_loop(3), that wait yields the fiber back to its loop instead of spinning the OS thread, so reader fibers get to run and the loop keeps serving other work. This behavior is already automatic in xtc_lrlock_publish(); an object created with either family works with either family's calls.

() records a deterministic operation and applies it immediately to the writer's copy. On (), the same op is replayed on the now-stale copy. Operations must be deterministic (same op + same starting state -> same end state). For non-deterministic mutations, use () which copies the entire data block instead of replaying ops.

() and () return direct pointers to the read and write copies; they are only safe to use while the caller holds writer ownership (between xtc_lrlock_write_begin(and) ()). () tells the lock that both copies were pre-initialised to the same state, skipping the first-publish full-sync.

Use xtc_lrlock when reads dominate (>50:1 R/W) and must never block. Memory cost is 2x the data size unless XTC_LRLOCK_COW is enabled. See xtc_lwlock(3) for short-critical-section reader/writer semantics with one copy.

struct config { int max_clients; int log_level; };
struct op_set_max { int new_max; };

static void apply_set_max(void *data, const void *op, size_t sz) {
    ((struct config *)data)->max_clients =
        ((const struct op_set_max *)op)->new_max;
    (void)sz;
}
static void copy_config(void *dst, const void *src, size_t sz) {
    memcpy(dst, src, sz);
}

xtc_lrlock_t *cfg_lock;
xtc_lrlock_create(sizeof(struct config), apply_set_max, copy_config,
                  "config", &cfg_lock);

/* Reader -- wait-free. */
const struct config *c = xtc_lrlock_read_begin(cfg_lock);
int n = c->max_clients;
xtc_lrlock_read_end(cfg_lock);

/* Writer -- serialised. */
xtc_lrlock_write_begin(cfg_lock);
struct op_set_max op = { .new_max = 100 };
xtc_lrlock_apply_op(cfg_lock, &op, sizeof op);
xtc_lrlock_publish(cfg_lock);
xtc_lrlock_write_end(cfg_lock);

SEE ALSO

xtc_lwlock(3), xtc_lockmgr(3), xtc_rcu(3), xtc(7)

Ported from PostgreSQL's lrlck branch. Appeared in xtc 0.1.

SEE ALSO

Pedro Ramalhete and Andreia Correia, ‘Left-Right: A Concurrency Control Technique with Wait-Free’ ‘Population Oblivious Reads’ (2014).

May 28, 2026 Debian

View the mdoc source