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 * @stability stable
4 * Use of this source code is governed by the ISC License.
5 *
6 * src/inc/xtc_lrlock.h
7 * Left-Right concurrency primitive: wait-free reads, single-writer
8 * with cooperative replay -- see Pedro Ramalhete &
9 * Andreia Correia, "Left-Right: A Concurrency Control Technique
10 * with Wait-Free Population Oblivious Reads" (2014).
11 *
12 * Two copies of the protected data live side-by-side. An atomic
13 * read_idx says which is currently the "read" copy.
14 *
15 * Reader path (wait-free):
16 * 1. set bit[me] in active_readers_mask (fetch_or)
17 * 2. epochs[me].epoch += 1 (becomes odd)
18 * 3. SeqCst fence
19 * 4. idx = atomic_read(read_idx)
20 * 5. read data[idx]
21 * 6. epochs[me].epoch += 1 (becomes even)
22 * 7. clear bit[me]
23 *
24 * Writer path:
25 * - acquire writer mutex
26 * - mutate data[1 - read_idx] either directly or via apply_op
27 * - publish:
28 * atomic_exchange(read_idx, 1 - read_idx)
29 * SeqCst fence
30 * snapshot epochs (skipping bits=0 in the active mask)
31 * wait until each snapshotted reader has advanced
32 * hybrid sync the now-stale copy:
33 * if oplog_count*256 <= data_size: replay oplog
34 * else: full sync
35 * - release writer mutex
36 *
37 * Trade-offs vs an RCU pointer:
38 * + No allocation in the publish path (the two copies are
39 * pre-allocated).
40 * + Deterministic reclamation (a single pointer swap).
41 * + Cache-locality preserved across mutations.
42 * - Two copies of the data; ~2x memory by default.
43 * - Writes are slower (applied twice; replay must be
44 * deterministic).
45 * - Single writer.
46 *
47 * xtc-specific extensions:
48 * - XTC_LRLOCK_COW: lazy second-copy allocation + MADV_FREE
49 * after publish. Idle steady state ~= 1x memory; first write
50 * after idle pays an mmap+memcpy.
51 */
52
53#ifndef XTC_LRLOCK_H
54#define XTC_LRLOCK_H
55
56#include "xtc_export.h"
57
58#include <stddef.h>
59#include <stdint.h>
60
61#include "xtc.h"
62
63typedef struct xtc_lrlock xtc_lrlock_t;
64
65/* Apply a single operation to one copy. Must be deterministic. */
66typedef void (*xtc_lrlock_apply_fn)(void *data, const void *op, size_t op_size);
67
68/* Synchronize destination from source -- used during first publish
69 * and during full-sync publishes. */
70typedef void (*xtc_lrlock_sync_fn)(void *dst, const void *src, size_t data_size);
71
72/* Flags for xtc_lrlock_create_ex(). */
73#define XTC_LRLOCK_COW (1u << 0) /* lazy data[1], MADV_FREE after publish */
74
75typedef struct xtc_lrlock_opts {
76 const char *name;
77 size_t data_size;
78 xtc_lrlock_apply_fn apply_fn;
79 xtc_lrlock_sync_fn sync_fn;
80 int max_readers; /* slot count; 0 -> 64 default */
81 size_t oplog_capacity; /* initial oplog bytes; 0 -> 4096 */
82 unsigned flags;
84
85/*
86 * PUBLIC: int xtc_lrlock_create __P((size_t, xtc_lrlock_apply_fn, xtc_lrlock_sync_fn, const char *, xtc_lrlock_t **));
87 * PUBLIC: int xtc_lrlock_create_ex __P((const xtc_lrlock_opts_t *, xtc_lrlock_t **));
88 * PUBLIC: void xtc_lrlock_destroy __P((xtc_lrlock_t *));
89 *
90 * PUBLIC: const void *xtc_lrlock_read_begin __P((xtc_lrlock_t *));
91 * PUBLIC: void xtc_lrlock_read_end __P((xtc_lrlock_t *));
92 *
93 * PUBLIC: void *xtc_lrlock_write_begin __P((xtc_lrlock_t *));
94 * PUBLIC: void xtc_lrlock_apply_op __P((xtc_lrlock_t *, const void *, size_t));
95 * PUBLIC: void xtc_lrlock_publish __P((xtc_lrlock_t *));
96 * PUBLIC: void xtc_lrlock_publish_full_sync __P((xtc_lrlock_t *));
97 * PUBLIC: void xtc_lrlock_write_end __P((xtc_lrlock_t *));
98 *
99 * PUBLIC: const void *xtc_lrlock_read_data __P((xtc_lrlock_t *));
100 * PUBLIC: void *xtc_lrlock_write_data __P((xtc_lrlock_t *));
101 * PUBLIC: void xtc_lrlock_mark_ready __P((xtc_lrlock_t *));
102 */
103
104/* Convenience wrapper: max_readers=64, oplog_capacity=4096, no flags. */
105XTC_API int xtc_lrlock_create(size_t data_size,
106 xtc_lrlock_apply_fn apply_fn,
107 xtc_lrlock_sync_fn sync_fn,
108 const char *name,
109 xtc_lrlock_t **out);
110
111XTC_API int xtc_lrlock_create_ex(const xtc_lrlock_opts_t *opts,
112 xtc_lrlock_t **out);
113
114XTC_API void xtc_lrlock_destroy(xtc_lrlock_t *lr);
115
116/* ---- reader (wait-free) ----
117 *
118 * read_begin returns the read-side data snapshot for the calling
119 * thread, registering a reader slot on first use. Reader slots are a
120 * fixed per-lock pool (max_readers, default 64; a global cap of 4096
121 * slots backs the registry). If the pool is exhausted -- more distinct
122 * reader threads than slots -- read_begin returns NULL; the caller must
123 * treat NULL as "retry later" (a thread that cannot get a slot should
124 * back off, not dereference). In practice size max_readers to the
125 * expected concurrent-reader-thread count. */
126XTC_API const void *xtc_lrlock_read_begin(xtc_lrlock_t *lr);
127XTC_API void xtc_lrlock_read_end(xtc_lrlock_t *lr);
128
129/* ---- writer (mutex-serialized) ---- */
130XTC_API void *xtc_lrlock_write_begin(xtc_lrlock_t *lr);
131XTC_API void xtc_lrlock_apply_op(xtc_lrlock_t *lr, const void *op, size_t op_size);
132XTC_API void xtc_lrlock_publish(xtc_lrlock_t *lr);
133
134/* Like publish, but unconditionally full-syncs (use when the writer
135 * mutated data directly, bypassing apply_op). */
136XTC_API void xtc_lrlock_publish_full_sync(xtc_lrlock_t *lr);
137
138XTC_API void xtc_lrlock_write_end(xtc_lrlock_t *lr);
139
140/* Direct accessors (only safe during writer ownership). */
141XTC_API const void *xtc_lrlock_read_data(xtc_lrlock_t *lr);
142XTC_API void *xtc_lrlock_write_data(xtc_lrlock_t *lr);
143
144/* Tell the lock both copies are pre-initialized to the same state.
145 * Skips the first-publish full-sync. */
146XTC_API void xtc_lrlock_mark_ready(xtc_lrlock_t *lr);
147
148/* ---- fiber-aware (async) left-right lock ------------------------------
149 *
150 * xtc_alrlock_* is the same left-right lock, named to make the
151 * fiber-awareness explicit at the call site. Reads are wait-free and
152 * never block, so there is nothing to make async on the read side; the
153 * ONE place a left-right lock waits is the WRITER's publish, which must
154 * wait for in-flight readers to advance past the version swap. When the
155 * writer runs inside a fiber (on an xtc loop), that wait yields the
156 * FIBER back to its loop instead of spinning the OS thread -- so the
157 * reader fibers get to run and the loop keeps serving other work.
158 *
159 * This behavior is automatic in xtc_lrlock_publish already; the
160 * xtc_alrlock_* names exist so a consumer building on fibers can express
161 * intent ("I want the fiber-aware left-right lock") and read back the
162 * guarantee. They are exact aliases -- an object created with either
163 * create function works with either family's calls.
164 *
165 * PUBLIC: int xtc_alrlock_create __P((size_t, xtc_lrlock_apply_fn, xtc_lrlock_sync_fn, const char *, xtc_lrlock_t **));
166 * PUBLIC: int xtc_alrlock_create_ex __P((const xtc_lrlock_opts_t *, xtc_lrlock_t **));
167 */
168XTC_API int xtc_alrlock_create(size_t data_size,
169 xtc_lrlock_apply_fn apply_fn,
170 xtc_lrlock_sync_fn sync_fn,
171 const char *name,
172 xtc_lrlock_t **out);
173XTC_API int xtc_alrlock_create_ex(const xtc_lrlock_opts_t *opts,
174 xtc_lrlock_t **out);
175
176#endif /* XTC_LRLOCK_H */