About

This post is a beginner-friendly introduction to mruby-task, a gem bundled with mruby that adds cooperative multitasking with preemptive scheduling. It lets you run multiple tasks concurrently inside a single mruby VM, each with its own stack and priority. The scheduler decides which task runs next, and you don't have to think about it most of the time. You just write your code as separate tasks and let the scheduler handle the switching.

Background

What is cooperative multitasking?

Cooperative multitasking means that tasks voluntarily yield control to let other tasks run. If a task never yields, it runs forever. With preemptive scheduling, the scheduler enforces a timeslice. If a task runs for too long without yielding, the scheduler preempts it and switches to another task. mruby-task combines both: you can yield explicitly with Task.pass, and the scheduler will also preempt a task after its timeslice expires (every 12ms by default).

What problem does it solve?

mruby normally runs one sequence of bytecode at a time. If you want to fetch data from a network while also responding to user input, you would normally need operating-system threads or processes. mruby-task solves this within a single VM: one task handles the event loop while another fetches data in the background. The two tasks share memory directly, so there is no need for inter-process communication or serialization.

Example #1

Hello, tasks

The simplest possible example is creating two tasks that print messages and yield to each other:

Task.new(name: "alice") do
  3.times do |i|
    puts "Alice: #{i}"
    Task.pass
  end
end

Task.new(name: "bob") do
  3.times do |i|
    puts "Bob: #{i}"
    Task.pass
  end
end

Task.run

Explanation

When you run this, you'll see the output alternate between Alice and Bob.

Example #2

Priorities

Tasks have a priority from 0 (highest) to 255 (lowest), defaulting to 128. The scheduler always picks the highest-priority ready task:

Task.new(name: "urgent", priority: 0) do
  puts "Urgent task runs first"
end

Task.new(name: "lazy", priority: 255) do
  puts "Lazy task runs last"
end

Task.run

Explanation

Priorities are useful when you have background work that should not delay more important tasks, such as keeping the UI responsive while a network request completes in the background.

Example #3

Sleep and yielding

When a task calls sleep, it is moved to a waiting queue and the scheduler picks the next ready task. No busy-waiting or polling is needed:

Task.new(name: "ticker") do
  loop do
    puts "tick"
    sleep 1
  end
end

Task.new(name: "reporter") do
  sleep 0.5
  puts "The ticker above me prints every second"
end

Task.run

Explanation

mruby-task also provides sleep_ms(milliseconds) and usleep(microseconds) for finer-grained control.

Example #4

Task::Queue (producer and consumer)

The Task::Queue class lets tasks communicate with each other. One task pushes items into the queue, and another task pops them. When the queue is empty, the consumer task blocks automatically. No polling:

q = Task::Queue.new
results = []

Task.new(name: "producer") do
  ["a", "b", "c"].each do |v|
    q.push(v)
    sleep 0.1
  end
  q.close
end

Task.new(name: "consumer") do
  loop do
    item = q.pop
    break if item.nil?
    results << item
  end
end

Task.run
puts results.inspect  # => ["a", "b", "c"]

Explanation

Non-blocking pops are also possible by passing true: q.pop(true). This raises Task::Error if the queue is empty.

Example #5

Joining tasks

join blocks the current task until another task finishes. This is useful when you need the result of a background computation:

worker = Task.new(name: "worker") do
  sleep 1
  42
end

Task.new(name: "main") do
  puts "Waiting for worker..."
  worker.join
  puts "Worker returned 42"
end

Task.run

Explanation

Note that join can only be called from within a task, and a task cannot join itself.

Configuration

Build setup

To use mruby-task in your project, add it to your build configuration:

MRuby::Build.new do |conf|
  conf.gem :core => 'mruby-task'
  # other gems...
end

Explanation

Timing

The scheduler fires a timer every 4ms (the tick unit) and gives each task a timeslice of 3 ticks (12ms). These can be overridden at compile time:

#define MRB_TICK_UNIT 4              // Tick period in milliseconds
#define MRB_TIMESLICE_TICK_COUNT 3   // Ticks per timeslice

Explanation

Task states

Each task can be in one of five states:

You can inspect the state of all tasks with Task.stat:

stats = Task.stat
puts stats[:ready][:count]     # number of ready tasks
puts stats[:waiting][:count]   # number of waiting tasks

Conclusion

mruby-task provides a simple, single-VM concurrency model that is well-suited for mruby applications that need to juggle multiple concerns: a UI event loop, background network requests, periodic timers, and so on. Task::Queue eliminates polling for inter-task communication, priorities let you control which work is more important, and the preemptive scheduler ensures that no single task can monopolise the CPU.

Further topics worth exploring include the Task::Queue blocking pop mechanic (which cooperatively yields the task while waiting), the mrb_task_run_once C API for integrating with foreign event loops, and writing custom HAL ports for platforms that are not POSIX or Windows.