Files
mruby-mruby/mrbgems/mruby-task
Yukihiro "Matz" Matsumoto 1d90c19a36 mruby-task: rename constants for improved readability
Renamed constants to use more descriptive underscores:
- MRB_TASKSTATUS_* -> MRB_TASK_STATUS_*
- MRB_TASKREASON_* -> MRB_TASK_REASON_*

This improves code readability by making the constant names clearer.

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-09 16:57:08 +09:00
..
2025-10-08 23:55:22 +09:00

mruby-task

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.

Purpose

The primary purpose of mruby-task is to enable mruby applications to:

  • Execute multiple tasks concurrently within a single VM.
  • Schedule tasks based on priority (0-255, where 0 is highest priority).
  • Provide cooperative yielding with Task.pass.
  • Support preemptive scheduling via timer-based interrupts.
  • Synchronize tasks using sleep and join operations.
  • Suspend and resume tasks programmatically.

Architecture

Task Scheduler

The scheduler uses four priority-sorted queues:

  • DORMANT: Tasks that have not started or have finished execution.
  • READY: Tasks ready to run, ordered by priority.
  • WAITING: Tasks waiting for sleep timeout or join completion.
  • SUSPENDED: Tasks manually suspended via #suspend.

Tick-Based Preemption

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:

  • A task's timeslice expires.
  • A task calls sleep, Task.pass, or join.
  • A task finishes execution.

Functionality

Creating Tasks

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 Class Methods

  • Task.new(name: nil, priority: 128) { block }: Creates and starts a new task. Lower priority values run first (0 is highest priority).

    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 scheduler statistics (tick count, wakeup time, task list).

    stats = Task.stat
    puts "Tick: #{stats.tick}"
    
  • 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 Instance Methods

  • #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.

    task.name = "worker-1"
    puts task.name  # => "worker-1"
    
  • #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"
    

Kernel Methods (Sleep)

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.

Configuration

Build Configuration

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 Configuration

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

Stack Configuration

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.

Platform Requirements

HAL (Hardware Abstraction Layer)

The task scheduler requires platform-specific timer and interrupt support via four HAL functions:

void mrb_task_hal_init(mrb_state *mrb);      // Initialize timer
void mrb_task_enable_irq(void);               // Enable timer interrupts
void mrb_task_disable_irq(void);              // Disable timer interrupts
void mrb_task_hal_idle_cpu(mrb_state *mrb);   // Idle when no tasks ready

POSIX Platform

On POSIX systems (Linux, macOS, BSD), the implementation uses:

  • SIGALRM signal for timer interrupts
  • setitimer() for periodic tick generation
  • sigprocmask() for interrupt enable/disable
  • SA_RESTART flag to prevent EINTR on system calls

Other Platforms

For embedded or non-POSIX platforms, you must implement the HAL functions. See the POSIX implementation in src/task.c as a reference.

Example for a hypothetical embedded platform:

void mrb_task_hal_init(mrb_state *mrb) {
  // Setup hardware timer to call mrb_tick() every MRB_TICK_UNIT ms
  hardware_timer_init(MRB_TICK_UNIT, timer_irq_handler);
}

void timer_irq_handler(void) {
  mrb_tick(global_mrb);  // Must be called from timer interrupt
}

Examples

Basic Multitasking

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

Priority Scheduling

# 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

Cooperative Yielding

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

Task Synchronization

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 Control

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

Limitations and Compatibility

Relationship with Fiber

Tasks and Fibers both use mrb_context but are not compatible:

  • Tasks are scheduled automatically by the preemptive scheduler.
  • Fibers require explicit Fiber.yield and resume calls.
  • Do not mix Tasks and Fibers in the same application.

Relationship with mruby-sleep

When mruby-task is enabled:

  • The sleep, usleep, and sleep_ms methods are task-aware.
  • Inside a task, they cooperatively yield to the scheduler.
  • Outside a task (or when scheduler is idle), they block.
  • mruby-sleep should be excluded from your build when using mruby-task.

Thread Safety

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.

Exceptions

Uncaught exceptions in a task will terminate that task but not affect other tasks. The exception is not propagated to the scheduler.

GC Integration

Task contexts are registered with the garbage collector. Tasks and their stacks/callinfo are properly marked and freed.

Testing

The gem includes tests that verify:

  • Task creation and execution
  • Priority scheduling
  • Sleep and wakeup
  • Join synchronization
  • Suspend and resume
  • Task.pass cooperative yielding

Run tests with:

rake CONFIG=host-debug test:lib

Implementation Details

Task States

Each task can be in one of five states:

  • DORMANT (0x00): Not started or finished
  • READY (0x02): Ready to run
  • RUNNING (0x03): Currently executing
  • WAITING (0x04): Waiting (sleep, join, mutex)
  • SUSPENDED (0x08): Manually suspended

Wait Reasons

When a task is in WAITING state, the reason indicates why:

  • NONE (0x00): No specific reason
  • SLEEP (0x01): Sleeping for time
  • MUTEX (0x02): Waiting for mutex (reserved, not yet implemented)
  • JOIN (0x04): Waiting for another task

Scheduler Algorithm

  1. Get the highest-priority task from the READY queue
  2. Set task status to RUNNING and switch context (mrb->c = &task->c)
  3. Execute task via mrb_vm_exec() until:
    • Task yields (sleep, pass, join)
    • Timeslice expires (preemption)
    • Task completes or terminates
  4. Handle completion (wake joined tasks, move to DORMANT)
  5. Run incremental GC if needed
  6. Requeue task to READY (if still running) or appropriate queue
  7. Repeat from step 1

Future Enhancements

Planned features not yet implemented:

  • Mutex support: Thread-safe synchronization primitives
  • Task.raise: Throw exceptions to other tasks
  • Task#value: Retrieve task return value (like Thread#value)
  • Per-task timeslice configuration

License

MIT License (same as mruby)

See Also

  • mruby-fiber: Cooperative fibers with manual control
  • mruby-sleep: Blocking sleep (superseded by mruby-task)