libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_lrlock.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * Use of this source code is governed by the ISC License.
4 *
5 * src/inc/xtc_lrlock.h
6 * Left-Right concurrency primitive: wait-free reads, single-writer
7 * with cooperative replay -- see Pedro Ramalhete &
8 * Andreia Correia, "Left-Right: A Concurrency Control Technique
9 * with Wait-Free Population Oblivious Reads" (2014).
10 *
11 * Two copies of the protected data live side-by-side. An atomic
12 * read_idx says which is currently the "read" copy.
13 *
14 * Reader path (wait-free):
15 * 1. set bit[me] in active_readers_mask (fetch_or)
16 * 2. epochs[me].epoch += 1 (becomes odd)
17 * 3. SeqCst fence
18 * 4. idx = atomic_read(read_idx)
19 * 5. read data[idx]
20 * 6. epochs[me].epoch += 1 (becomes even)
21 * 7. clear bit[me]
22 *
23 * Writer path:
24 * - acquire writer mutex
25 * - mutate data[1 - read_idx] either directly or via apply_op
26 * - publish:
27 * atomic_exchange(read_idx, 1 - read_idx)
28 * SeqCst fence
29 * snapshot epochs (skipping bits=0 in the active mask)
30 * wait until each snapshotted reader has advanced
31 * hybrid sync the now-stale copy:
32 * if oplog_count*256 <= data_size: replay oplog
33 * else: full sync
34 * - release writer mutex
35 *
36 * Trade-offs vs an RCU pointer:
37 * + No allocation in the publish path (the two copies are
38 * pre-allocated).
39 * + Deterministic reclamation (a single pointer swap).
40 * + Cache-locality preserved across mutations.
41 * - Two copies of the data; ~2x memory by default.
42 * - Writes are slower (applied twice; replay must be
43 * deterministic).
44 * - Single writer.
45 *
46 * xtc-specific extensions:
47 * - XTC_LRLOCK_COW: lazy second-copy allocation + MADV_FREE
48 * after publish. Idle steady state ~= 1x memory; first write
49 * after idle pays an mmap+memcpy.
50 */
51
52#ifndef XTC_LRLOCK_H
53#define XTC_LRLOCK_H
54
55#include <stddef.h>
56#include <stdint.h>
57
58#include "xtc.h"
59
60typedef struct xtc_lrlock xtc_lrlock_t;
61
62/* Apply a single operation to one copy. Must be deterministic. */
63typedef void (*xtc_lrlock_apply_fn)(void *data, const void *op, size_t op_size);
64
65/* Synchronize destination from source -- used during first publish
66 * and during full-sync publishes. */
67typedef void (*xtc_lrlock_sync_fn)(void *dst, const void *src, size_t data_size);
68
69/* Flags for xtc_lrlock_create_ex(). */
70#define XTC_LRLOCK_COW (1u << 0) /* lazy data[1], MADV_FREE after publish */
71
72typedef struct xtc_lrlock_opts {
73 const char *name;
74 size_t data_size;
75 xtc_lrlock_apply_fn apply_fn;
76 xtc_lrlock_sync_fn sync_fn;
77 int max_readers; /* slot count; 0 -> 64 default */
78 size_t oplog_capacity; /* initial oplog bytes; 0 -> 4096 */
79 unsigned flags;
81
82/*
83 * PUBLIC: int xtc_lrlock_create __P((size_t, xtc_lrlock_apply_fn, xtc_lrlock_sync_fn, const char *, xtc_lrlock_t **));
84 * PUBLIC: int xtc_lrlock_create_ex __P((const xtc_lrlock_opts_t *, xtc_lrlock_t **));
85 * PUBLIC: void xtc_lrlock_destroy __P((xtc_lrlock_t *));
86 *
87 * PUBLIC: const void *xtc_lrlock_read_begin __P((xtc_lrlock_t *));
88 * PUBLIC: void xtc_lrlock_read_end __P((xtc_lrlock_t *));
89 *
90 * PUBLIC: void *xtc_lrlock_write_begin __P((xtc_lrlock_t *));
91 * PUBLIC: void xtc_lrlock_apply_op __P((xtc_lrlock_t *, const void *, size_t));
92 * PUBLIC: void xtc_lrlock_publish __P((xtc_lrlock_t *));
93 * PUBLIC: void xtc_lrlock_publish_full_sync __P((xtc_lrlock_t *));
94 * PUBLIC: void xtc_lrlock_write_end __P((xtc_lrlock_t *));
95 *
96 * PUBLIC: const void *xtc_lrlock_read_data __P((xtc_lrlock_t *));
97 * PUBLIC: void *xtc_lrlock_write_data __P((xtc_lrlock_t *));
98 * PUBLIC: void xtc_lrlock_mark_ready __P((xtc_lrlock_t *));
99 */
100
101/* Convenience wrapper: max_readers=64, oplog_capacity=4096, no flags. */
102int xtc_lrlock_create(size_t data_size,
103 xtc_lrlock_apply_fn apply_fn,
104 xtc_lrlock_sync_fn sync_fn,
105 const char *name,
106 xtc_lrlock_t **out);
107
108int xtc_lrlock_create_ex(const xtc_lrlock_opts_t *opts,
109 xtc_lrlock_t **out);
110
111void xtc_lrlock_destroy(xtc_lrlock_t *lr);
112
113/* ---- reader (wait-free) ----
114 *
115 * read_begin returns the read-side data snapshot for the calling
116 * thread, registering a reader slot on first use. Reader slots are a
117 * fixed per-lock pool (max_readers, default 64; a global cap of 4096
118 * slots backs the registry). If the pool is exhausted -- more distinct
119 * reader threads than slots -- read_begin returns NULL; the caller must
120 * treat NULL as "retry later" (a thread that cannot get a slot should
121 * back off, not dereference). In practice size max_readers to the
122 * expected concurrent-reader-thread count. */
123const void *xtc_lrlock_read_begin(xtc_lrlock_t *lr);
124void xtc_lrlock_read_end(xtc_lrlock_t *lr);
125
126/* ---- writer (mutex-serialized) ---- */
127void *xtc_lrlock_write_begin(xtc_lrlock_t *lr);
128void xtc_lrlock_apply_op(xtc_lrlock_t *lr, const void *op, size_t op_size);
129void xtc_lrlock_publish(xtc_lrlock_t *lr);
130
131/* Like publish, but unconditionally full-syncs (use when the writer
132 * mutated data directly, bypassing apply_op). */
133void xtc_lrlock_publish_full_sync(xtc_lrlock_t *lr);
134
135void xtc_lrlock_write_end(xtc_lrlock_t *lr);
136
137/* Direct accessors (only safe during writer ownership). */
138const void *xtc_lrlock_read_data(xtc_lrlock_t *lr);
139void *xtc_lrlock_write_data(xtc_lrlock_t *lr);
140
141/* Tell the lock both copies are pre-initialized to the same state.
142 * Skips the first-publish full-sync. */
143void xtc_lrlock_mark_ready(xtc_lrlock_t *lr);
144
145#endif /* XTC_LRLOCK_H */