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 "xtc_export.h"
56
57#include <stddef.h>
58#include <stdint.h>
59
60#include "xtc.h"
61
62typedef struct xtc_lrlock xtc_lrlock_t;
63
64/* Apply a single operation to one copy. Must be deterministic. */
65typedef void (*xtc_lrlock_apply_fn)(void *data, const void *op, size_t op_size);
66
67/* Synchronize destination from source -- used during first publish
68 * and during full-sync publishes. */
69typedef void (*xtc_lrlock_sync_fn)(void *dst, const void *src, size_t data_size);
70
71/* Flags for xtc_lrlock_create_ex(). */
72#define XTC_LRLOCK_COW (1u << 0) /* lazy data[1], MADV_FREE after publish */
73
74typedef struct xtc_lrlock_opts {
75 const char *name;
76 size_t data_size;
77 xtc_lrlock_apply_fn apply_fn;
78 xtc_lrlock_sync_fn sync_fn;
79 int max_readers; /* slot count; 0 -> 64 default */
80 size_t oplog_capacity; /* initial oplog bytes; 0 -> 4096 */
81 unsigned flags;
83
84/*
85 * PUBLIC: int xtc_lrlock_create __P((size_t, xtc_lrlock_apply_fn, xtc_lrlock_sync_fn, const char *, xtc_lrlock_t **));
86 * PUBLIC: int xtc_lrlock_create_ex __P((const xtc_lrlock_opts_t *, xtc_lrlock_t **));
87 * PUBLIC: void xtc_lrlock_destroy __P((xtc_lrlock_t *));
88 *
89 * PUBLIC: const void *xtc_lrlock_read_begin __P((xtc_lrlock_t *));
90 * PUBLIC: void xtc_lrlock_read_end __P((xtc_lrlock_t *));
91 *
92 * PUBLIC: void *xtc_lrlock_write_begin __P((xtc_lrlock_t *));
93 * PUBLIC: void xtc_lrlock_apply_op __P((xtc_lrlock_t *, const void *, size_t));
94 * PUBLIC: void xtc_lrlock_publish __P((xtc_lrlock_t *));
95 * PUBLIC: void xtc_lrlock_publish_full_sync __P((xtc_lrlock_t *));
96 * PUBLIC: void xtc_lrlock_write_end __P((xtc_lrlock_t *));
97 *
98 * PUBLIC: const void *xtc_lrlock_read_data __P((xtc_lrlock_t *));
99 * PUBLIC: void *xtc_lrlock_write_data __P((xtc_lrlock_t *));
100 * PUBLIC: void xtc_lrlock_mark_ready __P((xtc_lrlock_t *));
101 */
102
103/* Convenience wrapper: max_readers=64, oplog_capacity=4096, no flags. */
104XTC_API int xtc_lrlock_create(size_t data_size,
105 xtc_lrlock_apply_fn apply_fn,
106 xtc_lrlock_sync_fn sync_fn,
107 const char *name,
108 xtc_lrlock_t **out);
109
110XTC_API int xtc_lrlock_create_ex(const xtc_lrlock_opts_t *opts,
111 xtc_lrlock_t **out);
112
113XTC_API void xtc_lrlock_destroy(xtc_lrlock_t *lr);
114
115/* ---- reader (wait-free) ----
116 *
117 * read_begin returns the read-side data snapshot for the calling
118 * thread, registering a reader slot on first use. Reader slots are a
119 * fixed per-lock pool (max_readers, default 64; a global cap of 4096
120 * slots backs the registry). If the pool is exhausted -- more distinct
121 * reader threads than slots -- read_begin returns NULL; the caller must
122 * treat NULL as "retry later" (a thread that cannot get a slot should
123 * back off, not dereference). In practice size max_readers to the
124 * expected concurrent-reader-thread count. */
125XTC_API const void *xtc_lrlock_read_begin(xtc_lrlock_t *lr);
126XTC_API void xtc_lrlock_read_end(xtc_lrlock_t *lr);
127
128/* ---- writer (mutex-serialized) ---- */
129XTC_API void *xtc_lrlock_write_begin(xtc_lrlock_t *lr);
130XTC_API void xtc_lrlock_apply_op(xtc_lrlock_t *lr, const void *op, size_t op_size);
131XTC_API void xtc_lrlock_publish(xtc_lrlock_t *lr);
132
133/* Like publish, but unconditionally full-syncs (use when the writer
134 * mutated data directly, bypassing apply_op). */
135XTC_API void xtc_lrlock_publish_full_sync(xtc_lrlock_t *lr);
136
137XTC_API void xtc_lrlock_write_end(xtc_lrlock_t *lr);
138
139/* Direct accessors (only safe during writer ownership). */
140XTC_API const void *xtc_lrlock_read_data(xtc_lrlock_t *lr);
141XTC_API void *xtc_lrlock_write_data(xtc_lrlock_t *lr);
142
143/* Tell the lock both copies are pre-initialized to the same state.
144 * Skips the first-publish full-sync. */
145XTC_API void xtc_lrlock_mark_ready(xtc_lrlock_t *lr);
146
147/* ---- fiber-aware (async) left-right lock ------------------------------
148 *
149 * xtc_alrlock_* is the same left-right lock, named to make the
150 * fiber-awareness explicit at the call site. Reads are wait-free and
151 * never block, so there is nothing to make async on the read side; the
152 * ONE place a left-right lock waits is the WRITER's publish, which must
153 * wait for in-flight readers to advance past the version swap. When the
154 * writer runs inside a fiber (on an xtc loop), that wait yields the
155 * FIBER back to its loop instead of spinning the OS thread -- so the
156 * reader fibers get to run and the loop keeps serving other work.
157 *
158 * This behavior is automatic in xtc_lrlock_publish already; the
159 * xtc_alrlock_* names exist so a consumer building on fibers can express
160 * intent ("I want the fiber-aware left-right lock") and read back the
161 * guarantee. They are exact aliases -- an object created with either
162 * create function works with either family's calls.
163 *
164 * PUBLIC: int xtc_alrlock_create __P((size_t, xtc_lrlock_apply_fn, xtc_lrlock_sync_fn, const char *, xtc_lrlock_t **));
165 * PUBLIC: int xtc_alrlock_create_ex __P((const xtc_lrlock_opts_t *, xtc_lrlock_t **));
166 */
167XTC_API int xtc_alrlock_create(size_t data_size,
168 xtc_lrlock_apply_fn apply_fn,
169 xtc_lrlock_sync_fn sync_fn,
170 const char *name,
171 xtc_lrlock_t **out);
172XTC_API int xtc_alrlock_create_ex(const xtc_lrlock_opts_t *opts,
173 xtc_lrlock_t **out);
174
175#endif /* XTC_LRLOCK_H */