Files
Asmod4n 17858cc5bd mruby-task GLib HAL
Lets mruby-task embed cleanly in any GLib-based event loop -- GTK,
libsoup, GStreamer, or anything else built on GMainContext. Tasks
become regular GSources, so the scheduler runs alongside whatever
else is on the loop without polling or busy-waiting.

Sleeping tasks cost zero CPU: the HAL parks until the next wakeup
deadline rather than ticking on a fixed cadence. Multiple mrb_states
on the same thread share one dispatcher and one ticker. Preemption,
Task.run, sleeper wakes, and foreign-loop integration all use the
same primitives, so embedders can mix Task.run with g_main_loop_run
freely.

ref mruby#6825
2026-05-17 13:44:20 +02:00

330 lines
9.3 KiB
C

/*
** mruby-task-demo.c
**
** Three-thread test of the mruby-task GLib HAL. Each thread owns its
** own mrb_state and its own GMainContext (the HAL is thread-local and
** picks up the thread-default context at mrb_open time, then spawns
** its own ticker thread internally).
**
** T1 pure foreign-loop driver. Tasks are registered, then
** g_main_loop_run is what dispatches the scheduler via the
** HAL's vm_run_src. No Task.run anywhere.
**
** T2 mix. Phase 1 registers tasks and calls Task.run to drain
** them synchronously. Phase 2 registers more tasks and lets
** g_main_loop_run drive them. Exercises both drivers on the
** same mrb_state in sequence.
**
** T3 Task.run only. Registers tasks and calls Task.run. The demo
** thread never enters g_main_loop_run -- Task.run is the
** scheduler driver, and the HAL's idle hook iterates vm_ctx
** from inside Task.run so the ticker's cross-thread wakes
** still get dispatched.
*/
#include <mruby.h>
#include <mruby/compile.h>
#include <mruby/error.h>
#include <mruby/string.h>
#include <mruby/variable.h>
#include <glib.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
static gint64 start_us;
static void
log_line(const char *msg)
{
gint64 ms = (g_get_monotonic_time() - start_us) / 1000;
printf("[t=%5" PRId64 " ms] %s\n", ms, msg);
fflush(stdout);
}
static mrb_value
rb_log(mrb_state *mrb, mrb_value self)
{
const char *msg;
(void)self;
mrb_get_args(mrb, "z", &msg);
log_line(msg);
return mrb_nil_value();
}
static void
run_ruby(mrb_state *mrb, const char *code)
{
mrb_load_string(mrb, code);
if (mrb->exc) {
mrb_value exc = mrb_obj_value(mrb->exc);
mrb_value str = mrb_funcall(mrb, exc, "to_s", 0);
fprintf(stderr, "Ruby error: %s\n", RSTRING_PTR(str));
fflush(stderr);
mrb->exc = NULL;
}
}
static void
banner(const char *msg)
{
printf("===== %s =====\n", msg);
fflush(stdout);
}
/*
* T1 -- pure foreign-loop driver. Runs on the main thread (no
* separate GThread for T1; the main thread is the foreign loop).
*
* Pulse task with mixed sleep styles, three staggered sleepers, and
* spinner + stopper for timeslice preemption. g_main_loop_run is the
* only scheduler driver.
*/
static void
run_glib_only(void)
{
GMainContext *ctx;
GMainLoop *loop;
GSource *timeout;
mrb_state *mrb;
ctx = g_main_context_new();
g_main_context_push_thread_default(ctx);
loop = g_main_loop_new(ctx, FALSE);
mrb = mrb_open();
mrb_define_method(mrb, mrb->object_class, "log", rb_log, MRB_ARGS_REQ(1));
banner("T1 (glib-only): pulse + 3 staggered sleepers + spinner/stopper");
run_ruby(mrb,
"$t1_done = false\n"
"Task.new(name: 'T1.pulse') {\n"
" log 'T1.pulse: usleep 8000 x5'\n"
" 5.times { usleep 8000; log 'T1.pulse: micro' }\n"
" log 'T1.pulse: sleep_ms 120 x2'\n"
" 2.times { sleep_ms 120; log 'T1.pulse: chunk' }\n"
" log 'T1.pulse: sleep 0.3'\n"
" sleep 0.3\n"
" log 'T1.pulse: long done'\n"
"}\n"
"[30, 60, 90].each do |ms|\n"
" Task.new(name: 'T1.s' + ms.to_s) {\n"
" log 'T1.sleeper' + ms.to_s + ': sleeping'\n"
" sleep_ms ms\n"
" log 'T1.sleeper' + ms.to_s + ': woke'\n"
" }\n"
"end\n"
"Task.new(name: 'T1.spinner', priority: 200) {\n"
" log 'T1.spinner: entering tight loop'\n"
" loops = 0\n"
" loop {\n"
" loops += 1\n"
" break if $t1_done\n"
" break if loops > 200_000_000\n"
" }\n"
" log 'T1.spinner: exit loops=' + loops.to_s + ' done=' + $t1_done.to_s\n"
"}\n"
"Task.new(name: 'T1.stopper', priority: 50) {\n"
" log 'T1.stopper: sleeping 100 ms'\n"
" sleep 0.1\n"
" log 'T1.stopper: setting $t1_done'\n"
" $t1_done = true\n"
"}\n"
);
timeout = g_timeout_source_new(700);
g_source_set_callback(timeout, (GSourceFunc)g_main_loop_quit, loop, NULL);
g_source_attach(timeout, ctx);
g_source_unref(timeout);
log_line("T1: entering g_main_loop_run (700 ms cap)");
g_main_loop_run(loop);
log_line("T1: g_main_loop_run returned");
mrb_close(mrb);
g_main_loop_unref(loop);
g_main_context_pop_thread_default(ctx);
g_main_context_unref(ctx);
}
/*
* T2 -- mixed driver.
*
* Phase 1: register a small task set, call Task.run, which blocks
* until those tasks drain. Phase 2: register more tasks and let
* g_main_loop_run drive them.
*/
static gpointer
thread_glib_and_taskrun(gpointer data)
{
GMainContext *ctx;
GMainLoop *loop;
GSource *timeout;
mrb_state *mrb;
(void)data;
ctx = g_main_context_new();
g_main_context_push_thread_default(ctx);
loop = g_main_loop_new(ctx, FALSE);
mrb = mrb_open();
mrb_define_method(mrb, mrb->object_class, "log", rb_log, MRB_ARGS_REQ(1));
banner("T2 (mix): phase 1 = yield + suspend/resume, drained by Task.run");
run_ruby(mrb,
"victim = Task.new(name: 'T2.victim') {\n"
" log 'T2.victim: sleeping 200 ms'\n"
" sleep 0.2\n"
" log 'T2.victim: woke'\n"
"}\n"
"Task.new(name: 'T2.controller', priority: 50) {\n"
" sleep 0.05\n"
" log 'T2.controller: suspending victim (was ' + victim.status.to_s + ')'\n"
" victim.suspend\n"
" log 'T2.controller: victim now ' + victim.status.to_s\n"
" sleep 0.1\n"
" log 'T2.controller: resuming victim (was ' + victim.status.to_s + ')'\n"
" victim.resume\n"
" log 'T2.controller: victim now ' + victim.status.to_s\n"
"}\n"
"Task.new(name: 'T2.yieldA', priority: 100) {\n"
" 3.times { |i| log 'T2.yieldA: iter ' + i.to_s; Task.pass }\n"
"}\n"
"Task.new(name: 'T2.yieldB', priority: 100) {\n"
" 3.times { |i| log 'T2.yieldB: iter ' + i.to_s; Task.pass }\n"
"}\n"
"log 'T2: calling Task.run (drains phase 1)'\n"
"Task.run\n"
"log 'T2: Task.run returned'\n"
);
banner("T2 (mix): phase 2 = 3 staggered sleepers, driven by g_main_loop_run");
run_ruby(mrb,
"[40, 80, 120].each do |ms|\n"
" Task.new(name: 'T2.s' + ms.to_s) {\n"
" log 'T2.sleeper' + ms.to_s + ': sleeping'\n"
" sleep_ms ms\n"
" log 'T2.sleeper' + ms.to_s + ': woke'\n"
" }\n"
"end\n"
"log 'T2: phase 2 registered'\n"
);
timeout = g_timeout_source_new(400);
g_source_set_callback(timeout, (GSourceFunc)g_main_loop_quit, loop, NULL);
g_source_attach(timeout, ctx);
g_source_unref(timeout);
log_line("T2: entering g_main_loop_run (400 ms cap)");
g_main_loop_run(loop);
log_line("T2: g_main_loop_run returned");
mrb_close(mrb);
g_main_loop_unref(loop);
g_main_context_pop_thread_default(ctx);
g_main_context_unref(ctx);
return NULL;
}
/*
* T3 -- Task.run only.
*
* No g_main_loop_run on the demo thread. Task.run drives the
* scheduler; mrb_hal_task_idle_cpu iterates vm_ctx from inside
* Task.run's idle loop so the ticker's cross-thread set_ready_time
* still wakes the demo thread when sleepers come due. Returns when
* all queues drain.
*/
static gpointer
thread_taskrun_only(gpointer data)
{
GMainContext *ctx;
mrb_state *mrb;
(void)data;
ctx = g_main_context_new();
g_main_context_push_thread_default(ctx);
mrb = mrb_open();
mrb_define_method(mrb, mrb->object_class, "log", rb_log, MRB_ARGS_REQ(1));
banner("T3 (Task.run only): zombie/executioner + spinner/stopper + sleepers");
run_ruby(mrb,
"$t3_done = false\n"
"$t3_zombie_ticks = 0\n"
"zombie = Task.new(name: 'T3.zombie') {\n"
" log 'T3.zombie: alive (will tick every 50 ms forever)'\n"
" loop {\n"
" sleep_ms 50\n"
" $t3_zombie_ticks += 1\n"
" log 'T3.zombie: tick ' + $t3_zombie_ticks.to_s\n"
" }\n"
" log 'T3.zombie: NEVER REACHED'\n"
"}\n"
"Task.new(name: 'T3.executioner', priority: 50) {\n"
" sleep_ms 175\n"
" log 'T3.executioner: terminating zombie (was ' + zombie.status.to_s + ')'\n"
" zombie.terminate\n"
" log 'T3.executioner: zombie is now ' + zombie.status.to_s\n"
"}\n"
"Task.new(name: 'T3.spinner', priority: 200) {\n"
" log 'T3.spinner: entering tight loop'\n"
" loops = 0\n"
" loop {\n"
" loops += 1\n"
" break if $t3_done\n"
" break if loops > 200_000_000\n"
" }\n"
" log 'T3.spinner: exit loops=' + loops.to_s + ' done=' + $t3_done.to_s\n"
"}\n"
"Task.new(name: 'T3.stopper', priority: 50) {\n"
" log 'T3.stopper: sleeping 100 ms'\n"
" sleep 0.1\n"
" log 'T3.stopper: setting $t3_done'\n"
" $t3_done = true\n"
"}\n"
"[20, 40, 60].each do |ms|\n"
" Task.new(name: 'T3.s' + ms.to_s) {\n"
" log 'T3.sleeper' + ms.to_s + ': sleeping'\n"
" sleep_ms ms\n"
" log 'T3.sleeper' + ms.to_s + ': woke'\n"
" }\n"
"end\n"
"log 'T3: calling Task.run'\n"
"Task.run\n"
"log 'T3: Task.run returned (all queues empty)'\n"
);
mrb_close(mrb);
g_main_context_pop_thread_default(ctx);
g_main_context_unref(ctx);
return NULL;
}
int main(int argc, char **argv)
{
GThread *t2, *t3;
(void)argc;
(void)argv;
start_us = g_get_monotonic_time();
log_line("main: spawning T2 + T3; running T1 (glib-only) on main thread");
t2 = g_thread_new("T2.mix", thread_glib_and_taskrun, NULL);
t3 = g_thread_new("T3.taskrun", thread_taskrun_only, NULL);
run_glib_only();
g_thread_join(t2);
g_thread_join(t3);
log_line("main: T2 and T3 joined");
printf("All scenarios completed.\n");
return 0;
}