Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
mutex_tests.h
Go to the documentation of this file.
1/* -*- C -*-
2 * Serene programming language
3 * Copyright (C) 2019-2026 Sameer Rahmani <lxsameer@lxsameer.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#pragma once
20
21#include <stdatomic.h>
22#include <stdint.h>
23
24#include "base.h"
25#include "serene/rt/fiber.h"
27
28#define MUTEX_TESTS(X) \
29 X("mutex::init", test_mutex_init), X("mutex::uncontended", test_mutex_uncontended), \
30 X("mutex::mutual_exclusion", test_mutex_mutual_exclusion), \
31 X("mutex::fifo_order", test_mutex_fifo_order), \
32 X("mutex::mt_contention", test_mutex_mt_contention)
33
36
37static inline srn_fiber_t *test_mtx_state(void) {
38 return atomic_load_explicit(&test_mtx.state, memory_order_relaxed);
39}
40
41// A freshly initialised lock is unlocked and has no waiters
42static void test_mutex_init() {
44 TEST_CHECK(test_mtx_state() == nullptr);
45 TEST_CHECK(test_mtx.waiters_head == nullptr);
46}
47
49 UNUSED(ctx);
50 UNUSED(arg);
52
55 // Re-acquiring a just-released lock must succeed.
59 return nullptr;
60}
61
63 MAKE_ENGINE(mm, engine);
64 MAKE_CONTEXT(engine, ctx);
65 srn_scheduler_t *sched = engine->scheduler;
66 ASSERT_NOT_NULL(sched);
67
69
71 srn_sched_run(sched, 1);
72
74 // The lock is released once the fiber is done.
75 TEST_CHECK(test_mtx_state() == nullptr);
76
77 RELEASE_CONTEXT(ctx);
78 SHUTDOWN_ENGINE(mm, engine);
79}
80
81// TL;DR: Holding a lock and yielding, is safe and protects the critical path.
82// N fibers on one worker each read a shared counter, yield while holding the lock, then write the
83// incremented value back. The yield inside the critical section is the trap, it hands the worker to
84// another fiber mid-update. Without the lock that peer would read the same stale value and an
85// update would be lost, leaving the total below N. The lock forces the peer to suspend at its own
86// lock() until the holder unlocks, so every increment lands.
87#define MTX_MUTEX_N 8
89
91 UNUSED(ctx);
92 UNUSED(arg);
93
95 // srn_fiber_mutex_lock(&test_mtx);
96 int seen = test_mtx_me_counter;
97 // Yield the execution and let other fibers run
99 test_mtx_me_counter = seen + 1;
100 // srn_fiber_mutex_unlock(&test_mtx);
101 });
102 return nullptr;
103}
104
106 MAKE_ENGINE(mm, engine);
107 MAKE_CONTEXT(engine, ctx);
108 srn_scheduler_t *sched = engine->scheduler;
109 ASSERT_NOT_NULL(sched);
110
113
114 for (int i = 0; i < MTX_MUTEX_N; i++) {
116 }
117 srn_sched_run(sched, 4);
118
119 // No update was lost despite each holder yielding in the middle of the criticalsection.
121 TEST_CHECK(test_mtx_state() == nullptr);
122
123 RELEASE_CONTEXT(ctx);
124 SHUTDOWN_ENGINE(mm, engine);
125}
126
127// Waiters are served oldest first. On one worker a holder takes the lock, then yields once. Then
128// Round robin runs the three waiters in arrival order and each blocks on lock(), so they queue as
129// 1, 2, 3. When the holder unlocks, the reversal in unlock hands the lock back in that same
130// order, so the recorded acquisition order is {1, 2, 3}. A LIFO stack would record {3, 2, 1}.
131#define MTX_FIFO_N 3
134
136 UNUSED(ctx);
137 int id = (int)(intptr_t)arg;
141 return nullptr;
142}
143
145 UNUSED(ctx);
146 UNUSED(arg);
148 // One yield is enough: the single worker runs all three waiters (each parking
149 // on lock) before this holder is scheduled again.
152 return nullptr;
153}
154
156 MAKE_ENGINE(mm, engine);
157 MAKE_CONTEXT(engine, ctx);
158 srn_scheduler_t *sched = engine->scheduler;
159 ASSERT_NOT_NULL(sched);
160
162 test_mtx_fifo_n = 0;
163
164 // Holder first so it owns the lock before any waiter runs, then the waiters
165 // in the order whose service order we assert.
166 (void)srn_fiber_spawn(ctx, test_mtx_fifo_holder_entry, nullptr);
167 for (int i = 0; i < MTX_FIFO_N; i++) {
168 (void)srn_fiber_spawn(ctx, test_mtx_fifo_waiter_entry, (void *)(intptr_t)(i + 1));
169 }
170 srn_sched_run(sched, 1);
171
173 for (int i = 0; i < MTX_FIFO_N; i++) {
174 TEST_CHECK(test_mtx_fifo_order[i] == i + 1);
175 TEST_MSG(
177 );
178 }
179 TEST_CHECK(test_mtx_state() == nullptr);
180
181 RELEASE_CONTEXT(ctx);
182 SHUTDOWN_ENGINE(mm, engine);
183}
184
185// Real parallelism. Many fibers across several worker threads each hammer the lock, doing a plain
186// (nonatomic) read, modify, write of a shared counter inside the critical section. The count is
187// only exact if the lock both excludes concurrent holders and publishes the previous holder's write
188// to the next one. Any missed exclusion or ordering shows up as a total. It is also the case TSan
189// watches for a data race on the plain counter.
190#define MTX_MT_N 64
191#define MTX_MT_ITERS 100
193
195 UNUSED(ctx);
196 UNUSED(arg);
197 for (int i = 0; i < MTX_MT_ITERS; i++) {
199 // plain RMW, guarded solely by the lock
202 }
203 return nullptr;
204}
205
207 MAKE_ENGINE(mm, engine);
208 MAKE_CONTEXT(engine, ctx);
209 srn_scheduler_t *sched = engine->scheduler;
210 ASSERT_NOT_NULL(sched);
211
214
215 for (int i = 0; i < MTX_MT_N; i++) {
216 (void)srn_fiber_spawn(ctx, test_mtx_mt_entry, nullptr);
217 }
218 srn_sched_run(sched, 4);
219
221 TEST_CHECK(test_mtx_state() == nullptr);
222
223 RELEASE_CONTEXT(ctx);
224 SHUTDOWN_ENGINE(mm, engine);
225}
#define TEST_CHECK(cond)
Definition acutest.h:95
#define TEST_MSG(...)
Definition acutest.h:223
#define RELEASE_CONTEXT(x)
Definition base.h:48
#define ASSERT_NOT_NULL(x)
Definition base.h:30
#define SHUTDOWN_ENGINE(mm, engine)
Definition base.h:40
#define MAKE_ENGINE(mm, engine)
Definition base.h:34
#define MAKE_CONTEXT(engine, x)
Definition base.h:44
srn_fiber_t * srn_fiber_spawn(srn_context_t *ctx, srn_fiber_entry_t entry, void *arg)
Make and schedule a fiber with every default, the engine's scheduler, the configured stack size,...
Definition fiber.c:250
AI Generated (🤦) Fiber subsystem overview.
void * srn_fiber_result_t
What a fiber's entry produces, type-erased.
Definition fiber.h:171
void srn_fiber_mutex_init(srn_fiber_mutex_t *m)
Initialise m to the unlocked state.
Definition mutex.c:76
void srn_fiber_mutex_unlock(srn_fiber_mutex_t *m)
Release m.
Definition mutex.c:99
void srn_fiber_mutex_lock(srn_fiber_mutex_t *m)
Acquire m.
Definition mutex.c:81
A fiber blocking mutex.
#define WITH_LOCK(lock, body)
Run body with lock held.
Definition mutex.h:84
static srn_fiber_result_t test_mtx_mt_entry(srn_context_t *ctx, void *arg)
static srn_fiber_result_t test_mtx_fifo_waiter_entry(srn_context_t *ctx, void *arg)
static int test_mtx_fifo_n
static srn_fiber_mutex_t test_mtx
Definition mutex_tests.h:34
static long test_mtx_mt_counter
static void test_mutex_init()
Definition mutex_tests.h:42
#define MTX_MT_ITERS
static int test_mtx_me_counter
Definition mutex_tests.h:88
#define MTX_FIFO_N
static srn_fiber_result_t test_mtx_me_entry(srn_context_t *ctx, void *arg)
Definition mutex_tests.h:90
static void test_mutex_mt_contention()
static srn_fiber_result_t test_mtx_fifo_holder_entry(srn_context_t *ctx, void *arg)
static void test_mutex_uncontended()
Definition mutex_tests.h:62
static srn_fiber_t * test_mtx_state(void)
Definition mutex_tests.h:37
static srn_fiber_result_t test_mtx_uncontended_entry(srn_context_t *ctx, void *arg)
Definition mutex_tests.h:48
#define MTX_MUTEX_N
Definition mutex_tests.h:87
static void test_mutex_mutual_exclusion()
static int test_mtx_fifo_order[MTX_FIFO_N]
#define MTX_MT_N
static bool test_mtx_uncontended_ran
Definition mutex_tests.h:35
static void test_mutex_fifo_order()
void srn_sched_run(srn_scheduler_t *sched, size_t nworkers)
Run the scheduler with nworkers os threads draining it, returning once the pool goes quiescent (every...
Definition scheduler.c:875
void srn_fiber_yield(void)
Yield cooperatively, re-enqueue the running fiber and run the next ready one.
Definition scheduler.c:1039
void * arg
#define UNUSED(x)
Definition utils.h:45