mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
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
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
MRuby::Build.new do |conf|
|
||||
# load specific toolchain settings
|
||||
conf.toolchain
|
||||
|
||||
# Use mrbgems
|
||||
# conf.gem 'examples/mrbgems/ruby_extension_example'
|
||||
# conf.gem 'examples/mrbgems/c_extension_example' do |g|
|
||||
# g.cc.flags << '-g' # append cflags in this gem
|
||||
# end
|
||||
# conf.gem 'examples/mrbgems/c_and_ruby_extension_example'
|
||||
# conf.gem :core => 'mruby-eval'
|
||||
# conf.gem :mgem => 'mruby-onig-regexp'
|
||||
# conf.gem :github => 'mattn/mruby-onig-regexp'
|
||||
# conf.gem :git => 'git@github.com:mattn/mruby-onig-regexp.git', :branch => 'master', :options => '-v'
|
||||
|
||||
# include the GEM box
|
||||
#conf.gembox 'default'
|
||||
|
||||
# C compiler settings
|
||||
# conf.cc do |cc|
|
||||
# cc.command = ENV['CC'] || 'gcc'
|
||||
# cc.flags = [ENV['CFLAGS'] || %w()]
|
||||
# cc.include_paths = ["#{root}/include"]
|
||||
# cc.defines = %w()
|
||||
# cc.option_include_path = %q[-I"%s"]
|
||||
# cc.option_define = '-D%s'
|
||||
# cc.compile_options = %Q[%{flags} -MMD -o "%{outfile}" -c "%{infile}"]
|
||||
# end
|
||||
|
||||
# mrbc settings
|
||||
# conf.mrbc do |mrbc|
|
||||
# mrbc.compile_options = "-g -B%{funcname} -o-" # The -g option is required for line numbers
|
||||
# end
|
||||
|
||||
# Linker settings
|
||||
# conf.linker do |linker|
|
||||
# linker.command = ENV['LD'] || 'gcc'
|
||||
# linker.flags = [ENV['LDFLAGS'] || []]
|
||||
# linker.flags_before_libraries = []
|
||||
# linker.libraries = %w()
|
||||
# linker.flags_after_libraries = []
|
||||
# linker.library_paths = []
|
||||
# linker.option_library = '-l%s'
|
||||
# linker.option_library_path = '-L%s'
|
||||
# linker.link_options = %Q[%{flags} -o "%{outfile}" %{objs} %{libs}]
|
||||
# end
|
||||
|
||||
# Archiver settings
|
||||
# conf.archiver do |archiver|
|
||||
# archiver.command = ENV['AR'] || 'ar'
|
||||
# archiver.archive_options = 'rs "%{outfile}" %{objs}'
|
||||
# end
|
||||
|
||||
# Parser generator settings
|
||||
# conf.yacc do |yacc|
|
||||
# yacc.command = ENV['YACC'] || 'bison'
|
||||
# yacc.compile_options = %q[-o "%{outfile}" "%{infile}"]
|
||||
# end
|
||||
|
||||
# gperf settings
|
||||
# conf.gperf do |gperf|
|
||||
# gperf.command = 'gperf'
|
||||
# gperf.compile_options = %q[-L ANSI-C -C -j1 -i 1 -o -t -N mrb_reserved_word -k"1,3,$" "%{infile}" > "%{outfile}"]
|
||||
# end
|
||||
|
||||
# file extensions
|
||||
# conf.exts do |exts|
|
||||
# exts.object = '.o'
|
||||
# exts.executable = '' # '.exe' if Windows
|
||||
# exts.library = '.a'
|
||||
# end
|
||||
|
||||
# file separator
|
||||
# conf.file_separator = '/'
|
||||
|
||||
# change library directory name from the default "lib" if necessary
|
||||
# conf.libdir_name = 'lib64'
|
||||
|
||||
# Turn on `enable_debug` for better debugging
|
||||
conf.enable_sanitizer 'address,undefined'
|
||||
conf.enable_debug
|
||||
conf.enable_bintest
|
||||
conf.enable_test
|
||||
conf.ports :glib
|
||||
conf.cc.defines << 'MRB_TASK_BUILD_DEMO'
|
||||
conf.gem core: 'mruby-task'
|
||||
conf.gem core: 'mruby-compiler'
|
||||
end
|
||||
@@ -13,4 +13,41 @@ MRuby::Gem::Specification.new('mruby-task') do |spec|
|
||||
if spec.for_windows?
|
||||
spec.linker.libraries << "winmm"
|
||||
end
|
||||
|
||||
ports = spec.build.effective_ports
|
||||
|
||||
# ports/glib/ needs glib-2.0 (GSource, GMainContext, GRecMutex) and
|
||||
# gthread-2.0 (GThread). On modern distros gthread-2.0 is a transparent
|
||||
# alias for glib-2.0; on older ones it's a separate .pc that pulls in
|
||||
# -lpthread, so query it separately.
|
||||
if ports.include?('glib')
|
||||
unless spec.search_package('glib-2.0') && spec.search_package('gthread-2.0')
|
||||
abort <<~MSG
|
||||
[mruby-task] conf.ports :glib selected but pkg-config could not find
|
||||
glib-2.0 / gthread-2.0. Install the GLib development headers
|
||||
(Debian/Ubuntu: libglib2.0-dev; Fedora: glib2-devel; Arch: glib2;
|
||||
macOS Homebrew: glib). For non-default install locations, set
|
||||
PKG_CONFIG_PATH before invoking rake.
|
||||
MSG
|
||||
end
|
||||
end
|
||||
|
||||
# Optional demo tool that exercises the GLib HAL end-to-end (basic
|
||||
# scheduling, priority ordering, timeslice preemption, auto-execution
|
||||
# under a foreign GLib main loop). Default off; opt in from your
|
||||
# build_config with:
|
||||
#
|
||||
# conf.cc.defines << 'MRB_TASK_BUILD_DEMO'
|
||||
# conf.ports :glib
|
||||
# conf.gem core: 'mruby-task'
|
||||
#
|
||||
# When enabled, `rake` produces bin/mruby-task-demo from
|
||||
# tools/mruby-task-demo/. The define is only inspected here -- the
|
||||
# demo's C source does not condition on it.
|
||||
if spec.build.cc.defines.include?('MRB_TASK_BUILD_DEMO')
|
||||
unless ports.include?('glib')
|
||||
abort '[mruby-task] MRB_TASK_BUILD_DEMO requires conf.ports :glib'
|
||||
end
|
||||
spec.bins = %w(mruby_task_demo)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
/*
|
||||
** task_hal.c - GLib HAL for mruby-task
|
||||
**
|
||||
** See Copyright Notice in mruby.h
|
||||
**
|
||||
** Drives the mruby-task scheduler from an embedding application's GLib
|
||||
** main loop. No Task.run is required from Ruby; the HAL fires
|
||||
** mrb_task_run_once and mrb_tick automatically as the host loop iterates,
|
||||
** which is the integration pattern a GTK or webview app uses in practice.
|
||||
**
|
||||
** Two GSources collaborate:
|
||||
**
|
||||
** 1. VM-run source on the thread-default GMainContext. Its callback
|
||||
** runs mrb_task_run_once on every registered VM.
|
||||
**
|
||||
** 2. Tick source on a dedicated GMainContext, iterated by a private
|
||||
** GThread. Its callback runs mrb_tick on every registered VM under
|
||||
** a recursive mutex. The separate thread is what gives us
|
||||
** preemption: it can fire mrb_tick while the VM thread is blocked
|
||||
** inside mrb_vm_exec.
|
||||
**
|
||||
** Both are manual GSources -- NULL prepare/check, dispatch driven purely
|
||||
** by ready_time updates via g_source_set_ready_time. g_timeout_source's
|
||||
** auto-reschedule would race with park-when-idle.
|
||||
**
|
||||
** State machine, evaluated after every dispatch under the IRQ lock:
|
||||
**
|
||||
** has_ready (any q_ready_) : vm_run = 0, tick = +1 interval
|
||||
** has_sleep (q_waiting_) : vm_run = -1 (parked), tick = soonest sleeper
|
||||
** neither : both = -1 (parked)
|
||||
**
|
||||
** In has_sleep state the ticker is the sole waker: it fires at the
|
||||
** sleeper deadline, catches the scheduler clock up via mrb_tick, and
|
||||
** sets vm_run = 0 once a task is promoted to ready. In neither state,
|
||||
** mrb_task_enable_irq is the wake: any Ruby-side scheduler activity
|
||||
** (Task.new from a bind callback, etc.) sets vm_run = 0 from outside.
|
||||
**
|
||||
** Tickless catch-up: in has_sleep state the ticker can be parked for
|
||||
** arbitrarily long. On fire we compute (now - last_fire_us) + remainder,
|
||||
** divide by MRB_TICK_INTERVAL_US, and call mrb_tick that many times in
|
||||
** one go. The leftover < interval is carried in remainder_us to the
|
||||
** next fire, keeping the scheduler clock aligned with monotonic time.
|
||||
** Net effect: a loop of long sleeps costs one wakeup per sleep period.
|
||||
**
|
||||
** Threading:
|
||||
** - Per-thread state lives in heap-allocated mrb_task_thread_state,
|
||||
** reachable via thread-local `ts`. The ticker thread receives a
|
||||
** pointer at spawn; it never touches another thread's TLS.
|
||||
** - On the main thread no GMainContext push is needed; the default
|
||||
** context is used implicitly.
|
||||
** - On any other thread that opens an mrb_state, the caller MUST first
|
||||
** call g_main_context_push_thread_default(). This is the standard
|
||||
** GLib convention used by libsoup, GIO async, GTask, etc.
|
||||
**
|
||||
** Locking:
|
||||
** - irq_lock (GRecMutex) covers every mutation of mrb->task state.
|
||||
** mrb_task_disable_irq / mrb_task_enable_irq are lock / unlock,
|
||||
** with the enable side additionally setting vm_run = 0.
|
||||
** - The ticker holds the lock across its mrb_tick batch.
|
||||
** - arm_locked runs with the lock held so a concurrent ticker can't
|
||||
** promote a sleeper between our inspection and our arm decision.
|
||||
** - mrb_vm_exec runs WITHOUT the lock; the ticker can preempt it
|
||||
** mid-execution by setting switching_.
|
||||
**
|
||||
** Supported platforms: any system with GLib 2.x and GThread (Linux,
|
||||
** BSD, macOS, Windows with MinGW or MSVC, ...).
|
||||
*/
|
||||
|
||||
#include <mruby.h>
|
||||
#include <mruby/error.h>
|
||||
#include "task.h"
|
||||
#include "task_hal.h"
|
||||
#include <glib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
|
||||
# define MRB_TASK_TLS _Thread_local
|
||||
#elif defined(__GNUC__) || defined(__clang__) || defined(__SUNPRO_C) || defined(__xlC__) || defined(__IBMC__)
|
||||
# define MRB_TASK_TLS __thread
|
||||
#elif defined(_MSC_VER) || defined(__BORLANDC__)
|
||||
# define MRB_TASK_TLS __declspec(thread)
|
||||
#else
|
||||
# error "mruby-task GLib HAL: no thread-local storage qualifier known for this compiler"
|
||||
#endif
|
||||
|
||||
#define MRB_TICK_INTERVAL_US ((gint64)MRB_TICK_UNIT * 1000)
|
||||
|
||||
typedef struct mrb_task_thread_state {
|
||||
mrb_state *vm_list[MRB_TASK_MAX_VMS];
|
||||
int vm_count;
|
||||
GRecMutex irq_lock;
|
||||
|
||||
GMainContext *vm_ctx;
|
||||
GSource *vm_run_src;
|
||||
|
||||
GMainContext *tick_ctx;
|
||||
GMainLoop *tick_loop;
|
||||
GSource *tick_src;
|
||||
GThread *ticker;
|
||||
|
||||
/* Tickless catch-up: last_fire_us is the monotonic anchor; remainder_us
|
||||
* is the sub-interval carry from the previous fire. */
|
||||
gint64 last_fire_us;
|
||||
uint32_t remainder_us;
|
||||
} mrb_task_thread_state;
|
||||
|
||||
static MRB_TASK_TLS mrb_task_thread_state *ts;
|
||||
|
||||
static gboolean
|
||||
deadline_source_dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
|
||||
{
|
||||
(void)source;
|
||||
if (!callback) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
return callback(user_data);
|
||||
}
|
||||
|
||||
static GSourceFuncs deadline_source_funcs = {
|
||||
NULL, NULL, deadline_source_dispatch, NULL, NULL, NULL,
|
||||
};
|
||||
|
||||
static void
|
||||
free_thread_state(mrb_task_thread_state *s)
|
||||
{
|
||||
if (!s) {
|
||||
return;
|
||||
}
|
||||
if (s->ticker) {
|
||||
if (s->tick_loop) {
|
||||
g_main_loop_quit(s->tick_loop);
|
||||
}
|
||||
(void)g_thread_join(s->ticker);
|
||||
s->ticker = NULL;
|
||||
}
|
||||
if (s->tick_src) {
|
||||
g_source_destroy(s->tick_src);
|
||||
g_source_unref(s->tick_src);
|
||||
s->tick_src = NULL;
|
||||
}
|
||||
if (s->tick_loop) {
|
||||
g_main_loop_unref(s->tick_loop);
|
||||
s->tick_loop = NULL;
|
||||
}
|
||||
if (s->tick_ctx) {
|
||||
g_main_context_unref(s->tick_ctx);
|
||||
s->tick_ctx = NULL;
|
||||
}
|
||||
if (s->vm_run_src) {
|
||||
g_source_destroy(s->vm_run_src);
|
||||
g_source_unref(s->vm_run_src);
|
||||
s->vm_run_src = NULL;
|
||||
}
|
||||
if (s->vm_ctx) {
|
||||
g_main_context_unref(s->vm_ctx);
|
||||
s->vm_ctx = NULL;
|
||||
}
|
||||
g_rec_mutex_clear(&s->irq_lock);
|
||||
g_free(s);
|
||||
}
|
||||
|
||||
/*
|
||||
* Walk every VM's queues, decide the arm state, apply it. Called with
|
||||
* the IRQ lock held. q_waiting_ is walked directly to find the soonest
|
||||
* SLEEP-reason wakeup; this is authoritative regardless of what
|
||||
* mrb->task.wakeup_tick currently reads.
|
||||
*
|
||||
* Idempotent. Deadlines are computed relative to s->last_fire_us (the
|
||||
* last actual ticker fire) rather than g_get_monotonic_time(), so
|
||||
* repeated calls between fires yield the same ready_time and don't
|
||||
* drift the cadence. set_ready_time is skipped when the value
|
||||
* wouldn't change, to avoid the eventfd-write side effect.
|
||||
*/
|
||||
static void
|
||||
arm_locked(mrb_task_thread_state *s)
|
||||
{
|
||||
gint64 monotonic_now = g_get_monotonic_time();
|
||||
int32_t min_wake_offset = 0;
|
||||
gboolean has_ready = FALSE;
|
||||
gboolean has_sleep = FALSE;
|
||||
gint64 new_vm_ready;
|
||||
gint64 new_tick_ready;
|
||||
gint64 cur_vm_ready;
|
||||
gint64 cur_tick_ready;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < s->vm_count; i++) {
|
||||
mrb_state *vm = s->vm_list[i];
|
||||
if (!vm) continue;
|
||||
|
||||
if (vm->task.queues[MRB_TASK_QUEUE_READY] != NULL) {
|
||||
has_ready = TRUE;
|
||||
continue;
|
||||
}
|
||||
|
||||
mrb_task *w = vm->task.queues[MRB_TASK_QUEUE_WAITING];
|
||||
while (w) {
|
||||
if (w->reason == MRB_TASK_REASON_SLEEP) {
|
||||
uint32_t wake = w->wait.wakeup_tick;
|
||||
uint32_t tick = vm->task.tick;
|
||||
int32_t offset = (int32_t)(wake - tick);
|
||||
if (!has_sleep || offset < min_wake_offset) {
|
||||
min_wake_offset = offset;
|
||||
has_sleep = TRUE;
|
||||
}
|
||||
}
|
||||
w = w->next;
|
||||
}
|
||||
}
|
||||
|
||||
/* Anchor the catch-up clock when the ticker transitions from parked
|
||||
* to active, so elapsed time counts from "now" rather than from
|
||||
* before the park. */
|
||||
if ((has_ready || has_sleep) &&
|
||||
g_source_get_ready_time(s->tick_src) == -1) {
|
||||
s->last_fire_us = monotonic_now;
|
||||
s->remainder_us = 0;
|
||||
}
|
||||
|
||||
if (has_ready) {
|
||||
new_vm_ready = 0;
|
||||
new_tick_ready = s->last_fire_us + MRB_TICK_INTERVAL_US;
|
||||
}
|
||||
else if (has_sleep) {
|
||||
new_vm_ready = -1;
|
||||
if (min_wake_offset <= 0) {
|
||||
new_tick_ready = 0; /* overdue, fire immediately */
|
||||
}
|
||||
else {
|
||||
new_tick_ready = s->last_fire_us +
|
||||
(gint64)min_wake_offset * MRB_TICK_INTERVAL_US;
|
||||
}
|
||||
}
|
||||
else {
|
||||
new_vm_ready = -1;
|
||||
new_tick_ready = -1;
|
||||
}
|
||||
|
||||
cur_vm_ready = g_source_get_ready_time(s->vm_run_src);
|
||||
cur_tick_ready = g_source_get_ready_time(s->tick_src);
|
||||
|
||||
if (new_vm_ready != cur_vm_ready) {
|
||||
g_source_set_ready_time(s->vm_run_src, new_vm_ready);
|
||||
}
|
||||
if (new_tick_ready != cur_tick_ready) {
|
||||
g_source_set_ready_time(s->tick_src, new_tick_ready);
|
||||
}
|
||||
}
|
||||
|
||||
static gpointer
|
||||
ticker_thread(gpointer data)
|
||||
{
|
||||
mrb_task_thread_state *s = (mrb_task_thread_state *)data;
|
||||
g_main_context_push_thread_default(s->tick_ctx);
|
||||
g_main_loop_run(s->tick_loop);
|
||||
g_main_context_pop_thread_default(s->tick_ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Tick GSource callback on the ticker thread. Computes catch-up ticks
|
||||
* from elapsed monotonic time plus carried remainder, calls mrb_tick
|
||||
* that many times under irq_lock, then lets arm_locked decide the
|
||||
* next state (steady-state cadence vs tickless deadline vs full park).
|
||||
*/
|
||||
static gboolean
|
||||
tick_source_cb(gpointer user_data)
|
||||
{
|
||||
mrb_task_thread_state *s = (mrb_task_thread_state *)user_data;
|
||||
int i;
|
||||
gint64 now = g_get_monotonic_time();
|
||||
gint64 total_us = (now - s->last_fire_us) + (gint64)s->remainder_us;
|
||||
gint64 raw_ticks = total_us / (gint64)MRB_TICK_INTERVAL_US;
|
||||
uint32_t catch_up_ticks = (raw_ticks > (gint64)UINT32_MAX)
|
||||
? UINT32_MAX
|
||||
: (uint32_t)raw_ticks;
|
||||
|
||||
s->remainder_us = (uint32_t)(total_us -
|
||||
(gint64)catch_up_ticks * (gint64)MRB_TICK_INTERVAL_US);
|
||||
s->last_fire_us = now;
|
||||
|
||||
g_rec_mutex_lock(&s->irq_lock);
|
||||
for (i = 0; i < s->vm_count; i++) {
|
||||
mrb_state *vm = s->vm_list[i];
|
||||
if (!vm) continue;
|
||||
for (uint32_t k = 0; k < catch_up_ticks; k++) {
|
||||
mrb_tick(vm);
|
||||
}
|
||||
}
|
||||
arm_locked(s);
|
||||
g_rec_mutex_unlock(&s->irq_lock);
|
||||
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* VM-run GSource callback on the VM thread. Snapshots vm_list under
|
||||
* the IRQ lock, runs mrb_task_run_once on each entry outside the
|
||||
* lock, then re-acquires to decide the next arm state. The snapshot
|
||||
* protects against list-shape changes while we're iterating (e.g., a
|
||||
* task body that registers or removes another mrb_state on this
|
||||
* thread).
|
||||
*/
|
||||
static gboolean
|
||||
vm_run_source_cb(gpointer user_data)
|
||||
{
|
||||
mrb_state *snapshot[MRB_TASK_MAX_VMS];
|
||||
int snapshot_count;
|
||||
int i;
|
||||
(void)user_data;
|
||||
|
||||
if (!ts) {
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
g_rec_mutex_lock(&ts->irq_lock);
|
||||
snapshot_count = ts->vm_count;
|
||||
memcpy(snapshot, ts->vm_list, (size_t)snapshot_count * sizeof(mrb_state *));
|
||||
g_rec_mutex_unlock(&ts->irq_lock);
|
||||
|
||||
for (i = 0; i < snapshot_count; i++) {
|
||||
if (snapshot[i]) {
|
||||
(void)mrb_task_run_once(snapshot[i]);
|
||||
}
|
||||
}
|
||||
|
||||
g_rec_mutex_lock(&ts->irq_lock);
|
||||
arm_locked(ts);
|
||||
g_rec_mutex_unlock(&ts->irq_lock);
|
||||
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
void
|
||||
mrb_hal_task_init(mrb_state *mrb)
|
||||
{
|
||||
int i;
|
||||
int idx = -1;
|
||||
gboolean first_on_thread = FALSE;
|
||||
guint attach_id;
|
||||
GError *err = NULL;
|
||||
gchar *err_msg;
|
||||
|
||||
for (i = 0; i < MRB_NUM_TASK_QUEUE; i++) {
|
||||
mrb->task.queues[i] = NULL;
|
||||
}
|
||||
mrb->task.tick = 0;
|
||||
mrb->task.wakeup_tick = UINT32_MAX;
|
||||
mrb->task.switching = FALSE;
|
||||
|
||||
if (ts == NULL) {
|
||||
ts = g_new0(mrb_task_thread_state, 1);
|
||||
g_rec_mutex_init(&ts->irq_lock);
|
||||
ts->last_fire_us = g_get_monotonic_time();
|
||||
first_on_thread = TRUE;
|
||||
}
|
||||
|
||||
g_rec_mutex_lock(&ts->irq_lock);
|
||||
|
||||
for (i = 0; i < ts->vm_count; i++) {
|
||||
if (ts->vm_list[i] == mrb) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (idx < 0) {
|
||||
if (ts->vm_count >= MRB_TASK_MAX_VMS) {
|
||||
g_rec_mutex_unlock(&ts->irq_lock);
|
||||
if (first_on_thread) {
|
||||
free_thread_state(ts);
|
||||
ts = NULL;
|
||||
}
|
||||
mrb_raisef(mrb, E_RUNTIME_ERROR,
|
||||
"too many mrb_states with task scheduler on this thread "
|
||||
"(max: %d)",
|
||||
MRB_TASK_MAX_VMS);
|
||||
}
|
||||
ts->vm_list[ts->vm_count++] = mrb;
|
||||
}
|
||||
|
||||
g_rec_mutex_unlock(&ts->irq_lock);
|
||||
|
||||
if (first_on_thread) {
|
||||
ts->vm_ctx = g_main_context_ref_thread_default();
|
||||
g_assert_nonnull(ts->vm_ctx);
|
||||
|
||||
ts->vm_run_src = g_source_new(&deadline_source_funcs, sizeof(GSource));
|
||||
g_assert_nonnull(ts->vm_run_src);
|
||||
g_source_set_callback(ts->vm_run_src, vm_run_source_cb, NULL, NULL);
|
||||
g_source_set_ready_time(ts->vm_run_src, -1);
|
||||
|
||||
attach_id = g_source_attach(ts->vm_run_src, ts->vm_ctx);
|
||||
if (attach_id == 0) {
|
||||
free_thread_state(ts);
|
||||
ts = NULL;
|
||||
mrb_raise(mrb, E_RUNTIME_ERROR,
|
||||
"mruby-task GLib HAL: g_source_attach failed for VM-run source");
|
||||
}
|
||||
|
||||
ts->tick_ctx = g_main_context_new();
|
||||
g_assert_nonnull(ts->tick_ctx);
|
||||
|
||||
ts->tick_loop = g_main_loop_new(ts->tick_ctx, FALSE);
|
||||
g_assert_nonnull(ts->tick_loop);
|
||||
|
||||
ts->tick_src = g_source_new(&deadline_source_funcs, sizeof(GSource));
|
||||
g_assert_nonnull(ts->tick_src);
|
||||
g_source_set_callback(ts->tick_src, tick_source_cb, ts, NULL);
|
||||
g_source_set_ready_time(ts->tick_src, -1);
|
||||
|
||||
attach_id = g_source_attach(ts->tick_src, ts->tick_ctx);
|
||||
if (attach_id == 0) {
|
||||
free_thread_state(ts);
|
||||
ts = NULL;
|
||||
mrb_raise(mrb, E_RUNTIME_ERROR,
|
||||
"mruby-task GLib HAL: g_source_attach failed for tick source");
|
||||
}
|
||||
|
||||
ts->ticker = g_thread_try_new("mruby-task-tick", ticker_thread, ts, &err);
|
||||
if (ts->ticker == NULL) {
|
||||
/* Copy GLib's error message into a stack buffer before any mruby
|
||||
* allocation, so mrb_raise's longjmp can't strand the GLib heap. */
|
||||
char buf[256];
|
||||
err_msg = g_strdup_printf(
|
||||
"mruby-task GLib HAL: failed to spawn ticker thread: %s",
|
||||
err ? err->message : "unknown error");
|
||||
g_strlcpy(buf, err_msg, sizeof(buf));
|
||||
g_free(err_msg);
|
||||
if (err) {
|
||||
g_error_free(err);
|
||||
}
|
||||
free_thread_state(ts);
|
||||
ts = NULL;
|
||||
mrb_raise(mrb, E_RUNTIME_ERROR, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
mrb_hal_task_final(mrb_state *mrb)
|
||||
{
|
||||
int i, j;
|
||||
gboolean last_on_thread = FALSE;
|
||||
|
||||
if (ts == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_rec_mutex_lock(&ts->irq_lock);
|
||||
|
||||
for (i = 0; i < ts->vm_count; i++) {
|
||||
if (ts->vm_list[i] == mrb) {
|
||||
for (j = i; j < ts->vm_count - 1; j++) {
|
||||
ts->vm_list[j] = ts->vm_list[j + 1];
|
||||
}
|
||||
ts->vm_list[ts->vm_count - 1] = NULL;
|
||||
ts->vm_count--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ts->vm_count == 0) {
|
||||
last_on_thread = TRUE;
|
||||
}
|
||||
|
||||
g_rec_mutex_unlock(&ts->irq_lock);
|
||||
|
||||
if (last_on_thread) {
|
||||
free_thread_state(ts);
|
||||
ts = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
mrb_task_disable_irq(void)
|
||||
{
|
||||
if (ts) {
|
||||
g_rec_mutex_lock(&ts->irq_lock);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hooked into the scheduler's IRQ-release path. After any state
|
||||
* change, re-evaluate the arm state of both sources via arm_locked.
|
||||
* This is what wires up preemption: arm_locked sets tick_src's
|
||||
* ready_time so the ticker thread fires mrb_tick on cadence.
|
||||
*
|
||||
* Without this, only vm_run_source_cb (the foreign-loop dispatch
|
||||
* callback) ever calls arm_locked, so a Task.run-driven scheduler
|
||||
* never arms the ticker -- preemption never happens, sleepers in
|
||||
* q_waiting_ are never woken, and CPU-bound tasks spin forever.
|
||||
* Calling arm_locked here covers both Task.run and foreign-loop
|
||||
* drivers symmetrically.
|
||||
*
|
||||
* arm_locked also sets vm_run_src ready_time to 0 only when there's
|
||||
* a ready task, which prevents the spurious wake during mrb_task_run's
|
||||
* idle queue check (the disable_irq/check/enable_irq pattern over a
|
||||
* read-only check produces no state change, so vm_run_src stays
|
||||
* parked at -1). */
|
||||
void
|
||||
mrb_task_enable_irq(void)
|
||||
{
|
||||
if (!ts) {
|
||||
return;
|
||||
}
|
||||
arm_locked(ts);
|
||||
g_rec_mutex_unlock(&ts->irq_lock);
|
||||
}
|
||||
|
||||
/* Called only from mrb_task_run's idle loop. Iterates the VM context
|
||||
* to dispatch any pending vm_run_src (e.g., the ticker just woke us
|
||||
* because a sleeper became ready), then returns. Block-wait is OK now
|
||||
* because mrb_task_enable_irq's q_ready_ guard prevents the spurious-
|
||||
* wake loop that would otherwise spin this iteration. */
|
||||
void
|
||||
mrb_hal_task_idle_cpu(mrb_state *mrb)
|
||||
{
|
||||
(void)mrb;
|
||||
if (ts && ts->vm_ctx) {
|
||||
(void)g_main_context_iteration(ts->vm_ctx, TRUE);
|
||||
}
|
||||
else {
|
||||
g_usleep(MRB_TICK_UNIT * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
mrb_hal_task_sleep_us(mrb_state *mrb, mrb_int usec)
|
||||
{
|
||||
(void)mrb;
|
||||
if (usec <= 0) {
|
||||
return;
|
||||
}
|
||||
g_usleep((gulong)usec);
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
** 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;
|
||||
}
|
||||
Reference in New Issue
Block a user