mrbgems/mruby-task/README.md
mruby-task is an mrbgem that provides cooperative multitasking with preemptive scheduling for mruby. It enables concurrent execution of multiple tasks within a single mruby VM instance using a priority-based scheduler with tick-based time slicing.
The primary purpose of mruby-task is to enable mruby applications to:
Task.pass.sleep and join operations.The scheduler uses four priority-sorted queues:
#suspend.A platform-specific timer generates periodic ticks (default: 4ms). Each task receives a timeslice (default: 3 ticks = 12ms) before being preempted. The scheduler automatically switches to the next ready task when:
sleep, Task.pass, or join.Tasks are created with Task.new and begin execution immediately:
# Create a task with default priority (128)
task = Task.new do
puts "Hello from task!"
sleep 1
puts "Task resumed"
end
# Create a named task with custom priority
task = Task.new(name: "worker", priority: 64) do
loop do
process_data
Task.pass # Yield to other tasks
end
end
# Start the scheduler (blocks until all tasks complete or idle)
Task.run
Task.new(name: nil, priority: 128) { block }: Creates and starts a new task. Lower priority values run first (0 is highest priority). The name parameter must be a String if provided. The priority must be an Integer between 0-255.
task = Task.new(name: "background", priority: 200) do
# Task code here
end
Task.current: Returns the currently executing task.
current = Task.current
puts "Running: #{current.name}"
Task.list: Returns an array of all tasks (including dormant tasks).
Task.list.each do |task|
puts "#{task.name}: #{task.status}"
end
Task.pass: Cooperatively yields execution to other ready tasks.
loop do
do_work
Task.pass # Let other tasks run
end
Task.get(name): Finds a task by name. Returns nil if not found.
worker = Task.get("worker")
worker.suspend if worker
Task.stat: Returns a hash containing scheduler statistics:
:tick (Integer): Current tick count:wakeup_tick (Integer): Next scheduled wakeup tick:dormant, :ready, :waiting, :suspended: Each is a hash with:
:count (Integer): Number of tasks in this queue:tasks (Array): Array of task objects in this queuestats = Task.stat
puts "Tick: #{stats[:tick]}"
puts "Ready tasks: #{stats[:ready][:count]}"
stats[:ready][:tasks].each { |t| puts t.name }
Task.run: Starts the scheduler main loop. Blocks until no tasks remain ready or waiting.
Task.new { do_async_work }
Task.run # Run scheduler until tasks complete
Task.tick: Returns the current tick count in milliseconds. This is the elapsed time since the scheduler started, measured in tick units.
start_tick = Task.tick
do_work
elapsed = Task.tick - start_tick
puts "Work took #{elapsed} ms"
#status: Returns the task status as a symbol (:DORMANT, :READY, :RUNNING, :WAITING, :SUSPENDED).
puts task.status # => :READY
#name / #name=: Get or set the task name. Returns "(noname)" for unnamed tasks. Note: name= accepts any value, but Task.new requires a String.
task.name = "worker-1"
puts task.name # => "worker-1"
task = Task.new { }
puts task.name # => "(noname)"
#priority / #priority=: Get or set the task priority (0-255). Changing priority requeues the task.
task.priority = 100 # Lower priority
#suspend: Suspends the task, moving it to the SUSPENDED queue. The task will not run until #resume is called.
task.suspend
# Later...
task.resume
#resume: Resumes a suspended task, moving it to the READY queue.
task.resume
#terminate: Terminates the task immediately, moving it to DORMANT state.
task.terminate
#join: Blocks the current task until the target task completes.
worker = Task.new { do_long_operation }
worker.join # Wait for completion
puts "Worker finished"
The task scheduler provides task-aware sleep methods that cooperatively yield to other tasks:
sleep(seconds): Sleeps for the specified duration. Accepts integers or floats (when MRB_NO_FLOAT is not defined).
sleep 1 # Sleep for 1 second
sleep 0.5 # Sleep for 500ms (with float support)
sleep # Sleep indefinitely (no arguments)
usleep(microseconds): Sleeps for the specified number of microseconds.
usleep 500000 # Sleep for 500ms
usleep 1000 # Sleep for 1ms
sleep_ms(milliseconds): Sleeps for the specified number of milliseconds.
sleep_ms 100 # Sleep for 100ms
Note: These methods override mruby-sleep when both gems are present. They
provide task-aware cooperative sleep when called from within a task, or
blocking sleep when called outside task context.
Enable the task scheduler by including the gem in your build config:
MRuby::Build.new do |conf|
# ... other configuration ...
conf.gem :core => 'mruby-task'
# ... other gems ...
end
This automatically defines MRB_USE_TASK_SCHEDULER.
Timing parameters can be configured via C defines:
#define MRB_TICK_UNIT 4 // Tick period in milliseconds (default: 4ms)
#define MRB_TIMESLICE_TICK_COUNT 3 // Ticks per timeslice (default: 3)
Default timeslice: MRB_TICK_UNIT * MRB_TIMESLICE_TICK_COUNT = 12ms
Task stack and call info sizes:
#define TASK_STACK_INIT_SIZE 64 // Initial stack entries (default: 64)
#define TASK_CI_INIT_SIZE 8 // Initial callinfo entries (default: 8)
These grow automatically as needed, similar to Fiber.
The task scheduler uses a Hardware Abstraction Layer (HAL) to support different platforms. Platform-specific timer and interrupt handling is provided by separate HAL gems.
hal-posix-task - For POSIX systems (Linux, macOS, BSD, Unix)
SIGALRM and setitimer() for timersigprocmask() for interrupt protectionSA_RESTART to prevent EINTR on system calls__EMSCRIPTEN__ defined), the SIGALRM timer is automatically disabled. JavaScript handles tick calls via setInterval, preventing double-increment of the tick counterhal-win-task - For Windows
timeSetEvent/timeKillEvent)CRITICAL_SECTION for interrupt protectionThe task scheduler will automatically select an appropriate HAL gem based on your platform. For explicit control, you can specify the HAL gem in your build configuration:
MRuby::Build.new do |conf|
# Option 1: Explicit HAL selection (recommended)
conf.gem core: 'hal-posix-task' # For Linux/macOS/BSD
# or
conf.gem core: 'hal-win-task' # For Windows
# mruby-task automatically loads if HAL is loaded
# But you can also specify it explicitly:
conf.gem core: 'mruby-task'
end
Auto-detection behavior:
mruby-task but no HAL gem, it will automatically load the appropriate HALhal-posix-taskhal-win-taskMulti-VM support:
mrb_state instancesMRB_TASK_MAX_VMS (default: 8)For embedded systems or unsupported platforms, you can create a custom HAL gem. The HAL must provide five functions defined in mruby-task/include/task_hal.h:
/**
* Initialize timer and register VM
* Called during gem initialization
* Must set up periodic timer to call mrb_tick(mrb) every MRB_TICK_UNIT ms
*/
void mrb_task_hal_init(mrb_state *mrb);
/**
* Cleanup timer and unregister VM
* Called during gem finalization
*/
void mrb_task_hal_final(mrb_state *mrb);
/**
* Enable timer interrupts (exit critical section)
* Must be reentrant for nested calls
*/
void mrb_task_enable_irq(void);
/**
* Disable timer interrupts (enter critical section)
* Must be reentrant for nested calls
*/
void mrb_task_disable_irq(void);
/**
* Put CPU in low-power/idle mode
* Called when no tasks are ready but some are waiting
* Should sleep ~MRB_TICK_UNIT milliseconds
*/
void mrb_task_hal_idle_cpu(mrb_state *mrb);
Example custom HAL gem structure:
mrbgems/hal-myplatform-task/
├── mrbgem.rake # Gem specification
├── include/
│ └── task_hal.h # Symlink to mruby-task/include/task_hal.h
└── src/
└── task_hal.c # Platform implementation
mrbgem.rake:
MRuby::Gem::Specification.new('hal-myplatform-task') do |spec|
spec.license = 'MIT'
spec.authors = 'Your Name'
spec.summary = 'My Platform HAL for mruby-task'
# HAL gem depends on feature gem (important for build order)
spec.add_dependency 'mruby-task', core: 'mruby-task'
# Add any platform-specific libraries or flags
# spec.linker.libraries << 'myplatform_timer'
end
task_hal.c example for embedded system:
#include <mruby.h>
#include "task_hal.h"
#include "myplatform_hardware.h"
static mrb_state *registered_vm = NULL;
void mrb_task_hal_init(mrb_state *mrb)
{
registered_vm = mrb;
// Setup hardware timer to fire every MRB_TICK_UNIT milliseconds
hardware_timer_init(MRB_TICK_UNIT, timer_isr);
hardware_timer_start();
}
void mrb_task_hal_final(mrb_state *mrb)
{
hardware_timer_stop();
registered_vm = NULL;
}
void mrb_task_enable_irq(void)
{
hardware_enable_interrupts();
}
void mrb_task_disable_irq(void)
{
hardware_disable_interrupts();
}
void mrb_task_hal_idle_cpu(mrb_state *mrb)
{
(void)mrb;
hardware_sleep_mode(); // Enter low-power mode until interrupt
}
// Timer ISR - must call mrb_tick() for scheduler
void timer_isr(void)
{
if (registered_vm) {
mrb_tick(registered_vm);
}
}
// Gem initialization (required but can be empty)
void mrb_hal_myplatform_task_gem_init(mrb_state *mrb)
{
(void)mrb;
}
void mrb_hal_myplatform_task_gem_final(mrb_state *mrb)
{
(void)mrb;
}
See hal-posix-task and hal-win-task source code for complete reference implementations.
The task scheduler provides a C API for integrating with C code and embedding environments. All exported functions are marked with MRB_API for external linkage.
/* Tick handler - called by timer interrupt */
MRB_API void mrb_tick(mrb_state *mrb);
/* Main scheduler loop - blocks until all tasks complete */
MRB_API mrb_value mrb_task_run(mrb_state *mrb);
/* Single-step task execution for event loop integration */
MRB_API mrb_value mrb_task_run_once(mrb_state *mrb);
mrb_task_run_once() executes one ready task and returns. This is designed for WASM/JavaScript event loop integration where the scheduler should yield control back to the browser between task executions.
/* Create a task from a proc */
MRB_API mrb_value mrb_create_task(mrb_state *mrb, struct RProc *proc,
mrb_value name, mrb_value priority,
mrb_value top_self);
Creates a new task from a RProc object. The name should be a String or mrb_nil_value(), priority should be an Integer (0-255) or mrb_nil_value() for default priority (128), and top_self sets the task's self object (or mrb_nil_value() to use default).
/* Suspend a task - prevents it from running until resumed */
MRB_API void mrb_suspend_task(mrb_state *mrb, mrb_value task);
/* Resume a suspended task - moves it back to ready/waiting queue */
MRB_API void mrb_resume_task(mrb_state *mrb, mrb_value task);
/* Terminate a task immediately - moves to dormant state */
MRB_API void mrb_terminate_task(mrb_state *mrb, mrb_value task);
/* Stop a task - marks as stopped without moving to dormant */
MRB_API mrb_bool mrb_stop_task(mrb_state *mrb, mrb_value task);
/* Get task result value */
MRB_API mrb_value mrb_task_value(mrb_state *mrb, mrb_value task);
/* Get task status symbol */
MRB_API mrb_value mrb_task_status(mrb_state *mrb, mrb_value task);
Note: These functions raise E_RUNTIME_ERROR if called during synchronous execution (when scheduler_lock > 0).
/* Execute a proc synchronously without context switching */
MRB_API mrb_value mrb_execute_proc_synchronously(mrb_state *mrb,
mrb_value proc,
mrb_int argc,
const mrb_value *argv);
This function creates a temporary task, executes it to completion, and returns the result. During execution, the scheduler is locked (scheduler_lock++), preventing any asynchronous task operations. This is designed for picoruby-wasm to execute Ruby code synchronously from JavaScript without triggering task switches.
Key characteristics:
/* Initialize task context with a new proc */
MRB_API void mrb_task_init_context(mrb_state *mrb, mrb_value task,
struct RProc *proc);
/* Reset task context to initial state */
MRB_API void mrb_task_reset_context(mrb_state *mrb, mrb_value task);
/* Set proc for task (without full reinitialization) */
MRB_API void mrb_task_proc_set(mrb_state *mrb, mrb_value task,
struct RProc *proc);
These functions are designed for picoruby-sandbox to reuse task objects for multiple executions without reallocating memory. mrb_task_init_context() fully reinitializes the context, while mrb_task_proc_set() only updates the proc pointer.
/* JavaScript calls this function periodically via setInterval */
void js_tick_callback(void) {
mrb_tick(mrb);
}
/* Main loop - called from JavaScript event loop */
mrb_value js_run_task_once(void) {
return mrb_task_run_once(mrb);
}
/* Execute Ruby code synchronously from JavaScript */
mrb_value js_eval_sync(const char *code) {
struct RProc *proc = mrb_generate_code(mrb, code);
return mrb_execute_proc_synchronously(mrb, mrb_obj_value(proc), 0, NULL);
}
/* Create a background task */
static mrb_value my_background_proc(mrb_state *mrb, mrb_value self) {
/* Task code here */
return mrb_nil_value();
}
void create_background_task(mrb_state *mrb) {
struct RProc *proc = mrb_proc_new_cfunc(mrb, my_background_proc);
mrb_value name = mrb_str_new_cstr(mrb, "background");
mrb_value priority = mrb_fixnum_value(128);
mrb_value task = mrb_create_task(mrb, proc, name, priority, mrb_nil_value());
}
Task.new(name: "task1") do
3.times do |i|
puts "Task 1: #{i}"
sleep 0.1
end
end
Task.new(name: "task2") do
3.times do |i|
puts "Task 2: #{i}"
sleep 0.1
end
end
Task.run # Run until both tasks complete
# High priority task (runs first)
Task.new(priority: 0) do
puts "High priority"
sleep 0.1
end
# Low priority task (runs after high priority yields)
Task.new(priority: 255) do
puts "Low priority"
end
Task.run
Task.new(name: "cooperative") do
loop do
do_some_work
Task.pass # Yield to other tasks
break if done?
end
end
Task.new(name: "other") do
do_other_work
end
Task.run
worker = Task.new(name: "worker") do
puts "Working..."
sleep 1
puts "Work done"
42 # Return value
end
Task.new(name: "main") do
puts "Waiting for worker..."
worker.join
puts "Worker completed!"
end
Task.run
task = Task.new do
loop do
puts "Running..."
sleep 0.5
end
end
# From another task or after Task.run returns:
task.suspend # Pause execution
sleep 1
task.resume # Resume execution
sleep 1
task.terminate # Stop permanently
Tasks and Fibers both use mrb_context but are not compatible:
Fiber.yield and resume calls.When mruby-task is enabled:
sleep, usleep, and sleep_ms methods are task-aware.mruby-sleep should be excluded from your build when using mruby-task.The task scheduler is not thread-safe. All tasks run in a single OS thread. For multi-core concurrency, use OS threads with separate mruby VMs per thread.
Uncaught exceptions in a task will terminate that task but not affect other tasks. The exception is not propagated to the scheduler.
Task contexts are registered with the garbage collector. Tasks and their stacks/callinfo are properly marked and freed.
The gem includes tests that verify:
Run tests with:
rake CONFIG=host-debug test:lib
Each task can be in one of five states:
DORMANT (0x00): Not started or finishedREADY (0x02): Ready to runRUNNING (0x03): Currently executingWAITING (0x04): Waiting (sleep, join, mutex)SUSPENDED (0x08): Manually suspendedWhen a task is in WAITING state, the reason indicates why:
NONE (0x00): No specific reasonSLEEP (0x01): Sleeping for timeMUTEX (0x02): Waiting for mutex (reserved, not yet implemented)JOIN (0x04): Waiting for another taskmrb->c = &task->c)mrb_vm_exec() until:
The scheduler includes a lock counter (mrb->task.scheduler_lock) that prevents asynchronous task operations during synchronous execution:
scheduler_lock > 0, asynchronous APIs (mrb_create_task, mrb_suspend_task, mrb_resume_task) raise E_RUNTIME_ERRORmrb_execute_proc_synchronously) completes without interferencePlanned features not yet implemented:
MIT License (same as mruby)
mruby-fiber: Cooperative fibers with manual controlmruby-sleep: Blocking sleep (superseded by mruby-task)