mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
fab6ccc794
Co-authored-by: Claude <noreply@anthropic.com>
476 lines
11 KiB
Markdown
476 lines
11 KiB
Markdown
# 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:
|
|
|
|
```ruby
|
|
# 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).
|
|
|
|
```ruby
|
|
task = Task.new(name: "background", priority: 200) do
|
|
# Task code here
|
|
end
|
|
```
|
|
|
|
- **`Task.current`**: Returns the currently executing task.
|
|
|
|
```ruby
|
|
current = Task.current
|
|
puts "Running: #{current.name}"
|
|
```
|
|
|
|
- **`Task.list`**: Returns an array of all tasks (including dormant tasks).
|
|
|
|
```ruby
|
|
Task.list.each do |task|
|
|
puts "#{task.name}: #{task.status}"
|
|
end
|
|
```
|
|
|
|
- **`Task.pass`**: Cooperatively yields execution to other ready tasks.
|
|
|
|
```ruby
|
|
loop do
|
|
do_work
|
|
Task.pass # Let other tasks run
|
|
end
|
|
```
|
|
|
|
- **`Task.get(name)`**: Finds a task by name. Returns `nil` if not found.
|
|
|
|
```ruby
|
|
worker = Task.get("worker")
|
|
worker.suspend if worker
|
|
```
|
|
|
|
- **`Task.stat`**: Returns scheduler statistics (tick count, wakeup time, task list).
|
|
|
|
```ruby
|
|
stats = Task.stat
|
|
puts "Tick: #{stats.tick}"
|
|
```
|
|
|
|
- **`Task.run`**: Starts the scheduler main loop. Blocks until no tasks remain ready or waiting.
|
|
|
|
```ruby
|
|
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`).
|
|
|
|
```ruby
|
|
puts task.status # => :READY
|
|
```
|
|
|
|
- **`#name`** / **`#name=`**: Get or set the task name.
|
|
|
|
```ruby
|
|
task.name = "worker-1"
|
|
puts task.name # => "worker-1"
|
|
```
|
|
|
|
- **`#priority`** / **`#priority=`**: Get or set the task priority (0-255). Changing priority requeues the task.
|
|
|
|
```ruby
|
|
task.priority = 100 # Lower priority
|
|
```
|
|
|
|
- **`#suspend`**: Suspends the task, moving it to the SUSPENDED queue. The task will not run until `#resume` is called.
|
|
|
|
```ruby
|
|
task.suspend
|
|
# Later...
|
|
task.resume
|
|
```
|
|
|
|
- **`#resume`**: Resumes a suspended task, moving it to the READY queue.
|
|
|
|
```ruby
|
|
task.resume
|
|
```
|
|
|
|
- **`#terminate`**: Terminates the task immediately, moving it to DORMANT state.
|
|
|
|
```ruby
|
|
task.terminate
|
|
```
|
|
|
|
- **`#join`**: Blocks the current task until the target task completes.
|
|
|
|
```ruby
|
|
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).
|
|
|
|
```ruby
|
|
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.
|
|
|
|
```ruby
|
|
usleep 500000 # Sleep for 500ms
|
|
usleep 1000 # Sleep for 1ms
|
|
```
|
|
|
|
- **`sleep_ms(milliseconds)`**: Sleeps for the specified number of milliseconds.
|
|
|
|
```ruby
|
|
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:
|
|
|
|
```ruby
|
|
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:
|
|
|
|
```c
|
|
#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:
|
|
|
|
```c
|
|
#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:
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
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
|
|
|
|
```ruby
|
|
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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
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
|
|
|
|
```ruby
|
|
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
|
|
|
|
```ruby
|
|
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:
|
|
|
|
```bash
|
|
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)
|