cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
scheduler.c
Go to the documentation of this file.
1
10#define _POSIX_C_SOURCE 200809L
11#include <stdatomic.h>
14#include <cwist/core/mem/gc.h>
15#include <pthread.h>
16#include <time.h>
17#include <stdlib.h>
18#include <string.h>
19#include <errno.h>
20
21#define CWIST_SCHEDULER_HEAP_INIT 16
22
23typedef struct {
24 uint64_t deadline_ms;
25 cwist_job_func func;
26 void *arg;
27} scheduled_job_t;
28
29struct cwist_scheduler {
30 cwist_io_queue *queue;
31 pthread_t *workers;
32 size_t worker_count;
33 pthread_t timer_thread;
34
35 scheduled_job_t *heap;
36 _Atomic size_t heap_count;
37 size_t heap_capacity;
38
39 pthread_mutex_t delayed_mtx;
40 pthread_cond_t delayed_cond;
41 /* Atomic: written by destroy and read by the timer thread and by
42 * cwist_scheduler_submit*() on any thread, not all under delayed_mtx. */
43 _Atomic bool running;
44};
45
46/* --- Time helpers ------------------------------------------------------- */
47
56static uint64_t now_ms(void) {
57 struct timespec ts;
58 if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
59 return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)ts.tv_nsec / 1000000ULL;
60}
61
68static void ms_to_timespec(uint64_t ms, struct timespec *ts) {
69 ts->tv_sec = (time_t)(ms / 1000ULL);
70 ts->tv_nsec = (long)((ms % 1000ULL) * 1000000ULL);
71}
72
82static void cond_init_monotonic(pthread_cond_t *cond) {
85#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
87#endif
88 pthread_cond_init(cond, &attr);
90}
91
92/* --- Min-heap of delayed jobs ------------------------------------------- */
93
105static int heap_grow(cwist_scheduler_t *s) {
106 if (atomic_load_explicit(&s->heap_count, memory_order_relaxed) < s->heap_capacity) return 0;
107 size_t new_cap = s->heap_capacity ? s->heap_capacity * 2 : CWIST_SCHEDULER_HEAP_INIT;
108 scheduled_job_t *new_heap = cwist_realloc(s->heap, new_cap * sizeof(scheduled_job_t));
109 if (!new_heap) return -1;
110 /* s->heap is owned by the scheduler for its whole lifetime, not by
111 * whichever caller thread happens to trigger a resize -- cwist_realloc()
112 * tracks it on that thread's full-GC pending-sweep list (matching
113 * cwist_alloc()'s tracking of a fresh NULL-in allocation). Left tracked
114 * there, that thread exiting would have the sweep free this array out
115 * from under a scheduler that's still using it. Disown it immediately;
116 * cwist_scheduler_destroy() is what actually releases it. */
118 s->heap = new_heap;
119 s->heap_capacity = new_cap;
120 return 0;
121}
122
126static void heap_swap(scheduled_job_t *a, scheduled_job_t *b) {
127 scheduled_job_t tmp = *a;
128 *a = *b;
129 *b = tmp;
130}
131
141static void heap_push(cwist_scheduler_t *s, scheduled_job_t job) {
142 if (heap_grow(s) != 0) return;
143 size_t i = atomic_load_explicit(&s->heap_count, memory_order_relaxed);
144 atomic_store_explicit(&s->heap_count, i + 1, memory_order_release);
145 s->heap[i] = job;
146 while (i > 0) {
147 size_t parent = (i - 1) / 2;
148 if (s->heap[parent].deadline_ms <= s->heap[i].deadline_ms) break;
149 heap_swap(&s->heap[parent], &s->heap[i]);
150 i = parent;
151 }
152}
153
161static scheduled_job_t heap_pop(cwist_scheduler_t *s) {
162 scheduled_job_t root = s->heap[0];
163 size_t count = atomic_load_explicit(&s->heap_count, memory_order_relaxed) - 1;
165 s->heap[0] = s->heap[count];
166 size_t i = 0;
167 while (1) {
168 size_t left = i * 2 + 1;
169 size_t right = left + 1;
170 size_t smallest = i;
171 if (left < count && s->heap[left].deadline_ms < s->heap[smallest].deadline_ms) {
172 smallest = left;
173 }
174 if (right < count && s->heap[right].deadline_ms < s->heap[smallest].deadline_ms) {
175 smallest = right;
176 }
177 if (smallest == i) break;
178 heap_swap(&s->heap[i], &s->heap[smallest]);
179 i = smallest;
180 }
181 return root;
182}
183
184/* --- Worker and timer threads ------------------------------------------- */
185
192static void *scheduler_worker_thread(void *arg) {
194 cwist_io_queue_run(s->queue);
195 return NULL;
196}
197
206static void *scheduler_timer_thread(void *arg) {
208
209 pthread_mutex_lock(&s->delayed_mtx);
210 while (s->running) {
211 if (atomic_load_explicit(&s->heap_count, memory_order_relaxed) == 0) {
212 pthread_cond_wait(&s->delayed_cond, &s->delayed_mtx);
213 continue;
214 }
215
216 uint64_t current_ms = now_ms();
217 if (s->heap[0].deadline_ms <= current_ms) {
218 scheduled_job_t job = heap_pop(s);
219 pthread_mutex_unlock(&s->delayed_mtx);
220 cwist_io_queue_submit(s->queue, job.func, job.arg);
221 pthread_mutex_lock(&s->delayed_mtx);
222 continue;
223 }
224
225 uint64_t wait_ms = s->heap[0].deadline_ms - current_ms;
226 struct timespec deadline;
227 ms_to_timespec(current_ms + wait_ms, &deadline);
228 pthread_cond_timedwait(&s->delayed_cond, &s->delayed_mtx, &deadline);
229 }
230 pthread_mutex_unlock(&s->delayed_mtx);
231 return NULL;
232}
233
244static void scheduler_cleanup_threads(cwist_scheduler_t *s, size_t started_workers) {
245 s->running = false;
246 cwist_io_queue_stop(s->queue);
247 for (size_t i = 0; i < started_workers; i++) {
248 pthread_join(s->workers[i], NULL);
249 }
250}
251
252/* --- Public API --------------------------------------------------------- */
253
266 if (worker_count == 0) worker_count = 1;
267
269 if (!s) return NULL;
270 /* Explicit destroy owns the scheduler, not its creator's TLS scope.
271 * Detach on the allocating thread before publishing to workers. */
273 memset(s, 0, sizeof(*s));
274
276 if (!s->queue) {
277 cwist_free(s);
278 return NULL;
279 }
280
281 s->workers = (pthread_t *)cwist_alloc_array(worker_count, sizeof(pthread_t));
282 if (!s->workers) {
283 cwist_io_queue_destroy(s->queue);
284 cwist_free(s);
285 return NULL;
286 }
287 s->worker_count = worker_count;
288
289 pthread_mutex_init(&s->delayed_mtx, NULL);
290 cond_init_monotonic(&s->delayed_cond);
291 s->running = true;
292
293 for (size_t i = 0; i < worker_count; i++) {
294 if (pthread_create(&s->workers[i], NULL, scheduler_worker_thread, s) != 0) {
295 scheduler_cleanup_threads(s, i);
296 pthread_mutex_destroy(&s->delayed_mtx);
297 pthread_cond_destroy(&s->delayed_cond);
298 cwist_free(s->workers);
299 cwist_io_queue_destroy(s->queue);
300 cwist_free(s);
301 return NULL;
302 }
303 }
304
305 if (pthread_create(&s->timer_thread, NULL, scheduler_timer_thread, s) != 0) {
306 scheduler_cleanup_threads(s, worker_count);
307 pthread_mutex_destroy(&s->delayed_mtx);
308 pthread_cond_destroy(&s->delayed_cond);
309 cwist_free(s->workers);
310 cwist_io_queue_destroy(s->queue);
311 cwist_free(s);
312 return NULL;
313 }
314
315 return s;
316}
317
328 if (!s) return;
329
330 s->running = false;
331 pthread_mutex_lock(&s->delayed_mtx);
332 pthread_cond_broadcast(&s->delayed_cond);
333 pthread_mutex_unlock(&s->delayed_mtx);
334
335 cwist_io_queue_stop(s->queue);
336
337 for (size_t i = 0; i < s->worker_count; i++) {
338 pthread_join(s->workers[i], NULL);
339 }
340 pthread_join(s->timer_thread, NULL);
341
342 cwist_io_queue_destroy(s->queue);
343 cwist_free(s->heap);
344 cwist_free(s->workers);
345 pthread_mutex_destroy(&s->delayed_mtx);
346 pthread_cond_destroy(&s->delayed_cond);
347 cwist_free(s);
348}
349
361 if (!s || !func || !s->running) return false;
362 return cwist_io_queue_submit(s->queue, func, arg);
363}
364
382 if (!s || !func || !s->running) return false;
383 if (delay_ms == 0) {
384 return cwist_io_queue_submit(s->queue, func, arg);
385 }
386
387 scheduled_job_t job;
388 job.deadline_ms = now_ms() + delay_ms;
389 job.func = func;
390 job.arg = arg;
391
392 pthread_mutex_lock(&s->delayed_mtx);
393 size_t before = atomic_load_explicit(&s->heap_count, memory_order_relaxed);
394 heap_push(s, job);
395 if (atomic_load_explicit(&s->heap_count, memory_order_relaxed) != before + 1) {
396 pthread_mutex_unlock(&s->delayed_mtx);
397 return false;
398 }
399 pthread_cond_signal(&s->delayed_cond);
400 pthread_mutex_unlock(&s->delayed_mtx);
401 return true;
402}
403
413 if (!s) return 0;
414 return atomic_load_explicit(&s->heap_count, memory_order_acquire);
415}
416
431 if (!s) return 0;
432
433 pthread_mutex_lock(&s->delayed_mtx);
434 scheduled_job_t *detached = s->heap;
435 size_t cancelled = atomic_load_explicit(&s->heap_count, memory_order_relaxed);
436 s->heap = NULL;
437 s->heap_capacity = 0;
439 pthread_mutex_unlock(&s->delayed_mtx);
440
441 if (drop_cb) {
442 for (size_t i = 0; i < cancelled; i++) {
443 drop_cb(detached[i].arg);
444 }
445 }
446 cwist_free(detached);
447 return cancelled;
448}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_realloc(void *ptr, size_t new_size)
Resize an existing allocation (libttak tracked).
Definition alloc.c:27
void * cwist_alloc_array(size_t count, size_t elem_size)
Allocate zeroed memory for count elements.
Definition alloc.c:21
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
void(* cwist_job_func)(void *arg)
Job function signature.
Definition cwist_io.h:22
struct cwist_io_queue cwist_io_queue
Opaque handle for the IO Queue. Backend implementation depends on OS (io_uring, kqueue,...
Definition cwist_io.h:16
void cwist_io_queue_run(cwist_io_queue *q)
Run the event loop. Blocks indefinitely, processing jobs as they complete/trigger.
Definition io_queue.c:293
void cwist_io_queue_stop(cwist_io_queue *q)
Request that all runners stop processing without freeing the queue.
Definition io_queue.c:331
void cwist_io_queue_destroy(cwist_io_queue *q)
Destroy the queue and free resources.
Definition io_queue.c:343
cwist_io_queue * cwist_io_queue_create(size_t capacity)
Initialize the job queue.
Definition io_queue.c:228
bool cwist_io_queue_submit(cwist_io_queue *q, cwist_job_func func, void *arg)
Submit a job to be executed asynchronously.
Definition io_queue.c:277
Epoch-based garbage collection wrapper.
bool cwist_gc_scope_disown(void *ptr)
Remove ptr from the current thread's pending-sweep list without freeing it, because ownership is bein...
Definition gc.c:766
bool cwist_full_gc_enabled(void)
Check whether full-GC mode is currently enabled.
Definition gc.c:515
size_t count
Definition otel.c:58
size_t cap
Definition otel.c:56
void cwist_scheduler_destroy(cwist_scheduler_t *s)
Destroy a scheduler and free all its resources.
Definition scheduler.c:327
bool cwist_scheduler_submit(cwist_scheduler_t *s, cwist_job_func func, void *arg)
Submit a job for immediate execution by a worker thread.
Definition scheduler.c:360
bool cwist_scheduler_schedule(cwist_scheduler_t *s, cwist_job_func func, void *arg, uint64_t delay_ms)
Schedule a job to run after a delay.
Definition scheduler.c:380
size_t cwist_scheduler_pending_count(cwist_scheduler_t *s)
Return the number of jobs waiting on the delayed-job heap.
Definition scheduler.c:412
size_t cwist_scheduler_cancel_pending(cwist_scheduler_t *s, cwist_scheduler_drop_cb drop_cb)
Cancel all delayed jobs that have not yet fired.
Definition scheduler.c:429
cwist_scheduler_t * cwist_scheduler_create(size_t worker_count, size_t queue_capacity)
Create a scheduler with worker and timer threads.
Definition scheduler.c:265
#define CWIST_SCHEDULER_HEAP_INIT
Definition scheduler.c:21
Background job scheduler / delayed task queue for CWIST.
struct cwist_scheduler cwist_scheduler_t
Opaque background job scheduler handle.
Definition scheduler.h:21
void(* cwist_scheduler_drop_cb)(void *arg)
Callback invoked for each pending job that is cancelled.
Definition scheduler.h:65