37 KiB
multi — Lua Cooperative Multitasking Library
Version 16.3.0 · MIT License · by Ryan Ward
Table of Contents
- Overview
- Core Concepts
- Getting Started
- The Main Loop
- Task Types (Actors)
- Threads
- Connections (Events)
- Processors
- Priority System
- Services
- Tasks (Deferred Work)
- Scheduled Jobs
- Global Variables & Thread Communication
- Type System
- Utility Functions
- Settings & Initialization
- System Events
- UUID Utilities
- Advanced Patterns
- Quick Reference Card
Overview
multi is a cooperative multitasking library for Lua. It provides a structured event loop, coroutine-based threads, typed connections (event emitters), and a rich set of timer and scheduling primitives — all without requiring OS threads or external dependencies.
The library is built around a single shared main loop that drives every actor (loop, alarm, step, thread, etc.) in turn. Because Lua is single-threaded, all concurrency is cooperative: tasks must yield control to let other tasks run.
Key design principles:
- All objects share a common interface (Pause, Resume, Destroy, setPriority, etc.).
- Connections decouple event producers from consumers.
- Threads are coroutines managed by the scheduler;
thread.sleepandthread.holdyield without blocking the loop. - Processors are isolated sub-schedulers that can be run inside threads or independently.
Core Concepts
| Concept | Description |
|---|---|
| Actor | Any object placed in the main loop that has an Act() method (loop, alarm, step, etc.). |
| Connection | An event channel. Producers call :Fire(...), consumers call :Connect(func). |
| Thread | A coroutine managed by the scheduler. Uses thread.sleep / thread.hold to yield. |
| Processor | An isolated scheduler with its own actor list and thread pool. |
| Priority | A numeric weight controlling how often an actor is executed in priority-mode mainloops. |
| Task | A one-shot deferred function queued via :newTask(func). |
Getting Started
local multi, thread = require("multi"):init()
-- Create a loop that fires every iteration
multi:newLoop(function(self, elapsed)
print("Elapsed:", elapsed)
end)
-- Create a timed loop that fires every 1 second
multi:newTLoop(function(self, ticks)
print("Tick:", ticks)
end, 1)
-- Start the scheduler (blocks until multi.Stop() is called)
multi:mainloop()
Initializing with Settings
local multi, thread = require("multi"):init({
print = true, -- enable multi.print() output
warn = true, -- enable multi.warn() output
debugging = false, -- enable multi.debug() output + debug manager
error = false, -- if true, hard-errors on runtime errors
priority = false, -- enable priority-based scheduling
findopt = false, -- enable optimization hints
})
init() returns multi and thread — always destructure both.
The Main Loop
The main loop drives every actor. There are two variants:
multi:mainloop()
Standard round-robin scheduler. Every actor is visited once per loop iteration in reverse insertion order.
multi:mainloop()
multi:p_mainloop()
Priority-based scheduler. Actors with higher priority are executed more frequently. Enable it via init({ priority = true }).
multi:uManager(dt?)
Runs a single pass of the loop manually. Useful when embedding multi inside another game loop or framework.
-- Inside LÖVE2D update callback:
function love.update(dt)
multi:uManager(dt)
end
multi.Stop()
Stops the main loop.
multi.Stop()
Task Types (Actors)
All actors are created on multi or on a processor. Every actor shares these common methods:
| Method | Description |
|---|---|
:Pause() |
Suspends the actor (its Act() is replaced with a no-op). |
:Resume() |
Resumes a paused actor. |
:Destroy() |
Removes the actor from the loop permanently. |
:setPriority(s) |
Sets the priority. Accepts string or number (see Priority System). |
:setName(name) |
Sets a human-readable name. |
:isPaused() |
Returns true if the actor is paused. |
:isActive() |
Returns true if the actor is active. |
newLoop
A loop that fires every scheduler iteration.
local loop = multi:newLoop(func?, notime?)
| Parameter | Type | Default | Description |
|---|---|---|---|
func |
function | nil | Connected to OnLoop immediately |
notime |
boolean | false | If true, elapsed is always nil |
Callback signature: function(self, elapsed, dt)
self— the loop objectelapsed— seconds since the loop was created (ornilifnotime = true)dt— delta time passed from the scheduler
Connections:
| Connection | Fires When |
|---|---|
OnLoop |
Every scheduler iteration |
local loop = multi:newLoop(function(self, elapsed, dt)
if elapsed > 5 then
print("5 seconds have passed!")
self:Destroy()
end
end)
newTLoop
A timed loop that fires at a fixed interval.
local tloop = multi:newTLoop(func?, interval?)
| Parameter | Type | Default | Description |
|---|---|---|---|
func |
function | nil | Connected to OnLoop immediately |
interval |
number | 0 | Seconds between firings |
Callback signature: function(self, ticks, dt)
ticks— total number of timesOnLoophas fired
Methods:
| Method | Description |
|---|---|
:Set(n) |
Change the interval |
:Pause() |
Pauses and freezes the internal timer |
:Resume() |
Resumes and unfreezes the internal timer |
-- Fire every 2 seconds
multi:newTLoop(function(self, ticks)
print("Tick #" .. ticks)
if ticks >= 10 then self:Destroy() end
end, 2)
newAlarm
A one-shot timer that fires after a delay and then pauses itself.
local alarm = multi:newAlarm(seconds?, func?)
| Parameter | Type | Default | Description |
|---|---|---|---|
seconds |
number | 0 | Delay in seconds |
func |
function | nil | Connected to OnRing immediately |
Connections:
| Connection | Fires When |
|---|---|
OnRing |
When the alarm expires |
Callback signature: function(self, dt)
Methods:
| Method | Description |
|---|---|
:Reset(n?) |
Restarts the alarm; optionally sets a new duration |
:Pause() |
Pauses (freezes remaining time) |
:Resume() |
Resumes from where it was paused |
multi:newAlarm(3, function(self)
print("3 seconds elapsed!")
self:Reset(3) -- reset for another 3 seconds
end)
newStep
A counter that steps through a range of values.
local step = multi:newStep(start?, reset?, count?, skip?)
| Parameter | Type | Default | Description |
|---|---|---|---|
start |
number | 1 | Starting value |
reset |
number | math.huge | Ending value (inclusive) |
count |
number | 1 | Increment per step |
skip |
number | 0 | Number of loop iterations to skip between steps |
Connections:
| Connection | Fires When |
|---|---|
OnStart |
When the step position is at start |
OnStep |
Every step; receives (self, position, dt) |
OnEnd |
When position reaches reset |
Methods:
| Method | Description |
|---|---|
:Update(start, reset, count, skip) |
Update parameters and resume |
:Count(n) |
Change the step increment |
:Break() |
Hard-stop (sets Active = nil) |
multi:newStep(1, 5, 1):OnStep(function(self, pos, dt)
print("Step:", pos)
end)
newTStep
A timed step — same as newStep but advances on a time interval rather than every loop iteration.
local tstep = multi:newTStep(start?, reset?, count?, interval?)
| Parameter | Type | Default | Description |
|---|---|---|---|
interval |
number | 1 | Seconds between steps |
Methods: Same as newStep, plus:
| Method | Description |
|---|---|
:Set(n) |
Change the interval |
:Reset(n?) |
Restart and optionally update interval |
-- Count from 1 to 10, one step per second
multi:newTStep(1, 10, 1, 1):OnStep(function(self, pos)
print("Position:", pos)
end)
newEvent
An actor that polls a function and fires when it returns a truthy value.
local event = multi:newEvent(task?, func?)
| Parameter | Type | Description |
|---|---|---|
task |
function | Polled every iteration; should return a value when "done" |
func |
function | Connected to OnEvent immediately |
Connections:
| Connection | Fires When |
|---|---|
OnEvent |
When task() returns a truthy value |
Callback signature: function(self, dt)
The return value of task() is stored in self.returns.
Methods:
| Method | Description |
|---|---|
:SetTask(func) |
Replace the polling function |
local flag = false
multi:newEvent(function() return flag end, function(self)
print("Flag was set!")
end)
-- Somewhere else in the code:
flag = true
newUpdater
An actor that fires on every N-th loop iteration (frame skip).
local updater = multi:newUpdater(skip?, func?)
| Parameter | Type | Default | Description |
|---|---|---|---|
skip |
number | 1 | Fire every skip iterations |
func |
function | nil | Connected to OnUpdate |
Connections:
| Connection | Fires When |
|---|---|
OnUpdate |
Every skip iterations |
Methods:
| Method | Description |
|---|---|
:SetSkip(n) |
Change the skip interval |
-- Fire every 10 iterations
multi:newUpdater(10, function(self, dt)
print("Every 10 ticks")
end)
newTimer
A utility timer object (not an actor — does not run in the main loop).
local timer = multi:newTimer()
Methods:
| Method | Returns | Description |
|---|---|---|
:Start() |
self | Start (or restart) the timer |
:Get() |
number | Elapsed seconds |
:Pause() |
self | Freeze elapsed time |
:Resume() |
self | Continue from frozen time |
:isPaused() |
bool | Whether the timer is paused |
:Reset() is an alias for :Start().
local t = multi:newTimer()
t:Start()
multi:newLoop(function()
if t:Get() > 5 then
print("5 seconds passed")
t:Stop()
end
end)
newTimeout
Creates a one-shot connection that fires after a delay, then destroys the receiving object.
local timeout = multi:newTimeout(seconds)
Returns a connection-modifier function. Used in combination with connection chaining.
-- This pattern pauses self after 5 seconds
multi:newTimeout(5)(function(self)
print("Timed out!")
end)
Threads
Threads are coroutines managed by the scheduler. They are created with thread:newThread and run cooperatively alongside all other actors.
thread.newThread
local th = thread:newThread(name?, func, ...)
| Parameter | Type | Description |
|---|---|---|
name |
string | Optional display name |
func |
function | The coroutine body |
... |
any | Arguments passed to func on first resume |
Connections on the returned thread:
| Connection | Fires When |
|---|---|
OnDeath |
Thread function returns normally; receives return values |
OnError |
Thread function throws an error; receives (self, errorMsg) |
Methods:
| Method | Description |
|---|---|
:Pause() |
Pause the thread at its next yield point |
:Resume() |
Resume a paused thread |
:Kill() |
Kill the thread at its next yield point |
:Sleep(n) |
Request the thread sleep for n seconds at next yield |
:Hold(func, opt?) |
Request the thread hold until func returns true |
:getName() |
Returns the thread's name |
:isPaused() |
Returns true if paused |
thread:newThread("MyThread", function(a, b)
print("Got:", a, b)
thread.sleep(1)
print("1 second later")
return "done"
end, "hello", "world"):OnDeath(function(result)
print("Result:", result) -- "done"
end)
thread.sleep / thread.hold
These are the primary yield mechanisms inside threads.
thread.sleep(seconds)
Yields the thread for a fixed duration.
thread:newThread("Waiter", function()
print("Before sleep")
thread.sleep(2)
print("After 2 seconds")
end)
thread.hold(condition, opts?)
Yields until a condition is true. The condition is polled every scheduler pass.
-- Hold until a flag is set
local ready = false
thread:newThread("Holder", function()
thread.hold(function() return ready end)
print("Ready!")
end)
-- Hold until a connection fires
local conn = multi:newConnection()
thread:newThread("ConnHolder", function()
local value = thread.hold(conn)
print("Connection fired with:", value)
end)
-- Hold with a numeric timeout
thread:newThread("WithTimeout", function()
local result, timeout = thread.hold(function()
return someCondition()
end, { sleep = 5 })
if multi.isTimeout(timeout) then
print("Timed out!")
end
end)
opts table fields:
| Field | Description |
|---|---|
sleep |
Max seconds to wait before returning nil, TIMEOUT |
cycles |
Max loop iterations to wait before returning nil, TIMEOUT |
skip |
Skip N iterations between condition checks |
interval |
Minimum seconds between condition checks |
thread.yield / thread.skip
thread.yield()
Yields for exactly one scheduler pass (minimum possible pause).
thread:newThread("Yielder", function()
for i = 1, 1000 do
doSomeWork(i)
thread.yield() -- give other threads a turn each iteration
end
end)
thread.skip(n)
Yields for exactly n scheduler passes.
thread.skip(5) -- pause for 5 iterations
thread.holdFor / thread.holdWithin
thread.holdFor(seconds, condition?)
Hold for up to seconds seconds, optionally with a condition function.
thread.holdFor(3, function() return isReady() end)
thread.holdWithin(cycles, condition?)
Hold for up to cycles iterations with an optional condition.
thread.holdWithin(100, function() return isReady() end)
thread.newISOThread
Creates an isolated thread with its own environment (useful for sandboxing).
local th = thread:newISOThread(name?, func, env?, ...)
| Parameter | Type | Description |
|---|---|---|
env |
table | The environment table. thread and multi are injected unless already present. |
thread:newISOThread("Isolated", function()
thread.sleep(1)
print("This runs in isolation")
end, { print = print })
thread.newFunction
Wraps a function as a threaded callable — calling it spawns a thread and optionally waits for its result.
local tfunc = thread:newFunction(func, holdme?)
| Parameter | Type | Description |
|---|---|---|
func |
function | The function body |
holdme |
boolean | If true, calling the TFunc blocks until it returns |
Returns a TFunc object. Calling the TFunc spawns a thread and returns a handle.
Handle methods:
| Method | Description |
|---|---|
:wait() |
Block (or hold in a thread) until the function returns |
:connect(func) |
Call func with the return values when done |
.OnReturn |
Connection that fires when done |
.OnError |
Connection that fires on error |
.OnStatus |
Connection for thread.pushStatus(...) values |
local fetchData = thread:newFunction(function(url)
thread.sleep(1) -- simulate async work
return "data from " .. url
end)
-- Non-blocking call:
local handle = fetchData("http://example.com")
handle:connect(function(result)
print(result)
end)
-- Blocking call (holdme = true):
local fetchSync = thread:newFunction(function(url)
thread.sleep(1)
return "sync data"
end, true)
thread:newThread("caller", function()
local result = fetchSync("http://example.com")
print(result)
end)
Connections (Events)
Connections are the event system of multi. They decouple event producers from consumers.
newConnection
local conn = obj:newConnection(protect?, func?, kill?)
| Parameter | Type | Description |
|---|---|---|
protect |
boolean | If true, each callback is wrapped in pcall |
func |
function | Immediately connected callback |
kill |
boolean | If true, callbacks are removed after first call (one-shot) |
Fire / Connect / Unconnect
:Fire(...)
Broadcasts values to all connected callbacks.
local onClick = multi:newConnection()
onClick:Fire("button1", 42)
:Connect(func, name?)
Subscribes a function. Returns a connection handle.
local handle = onClick:Connect(function(button, value)
print(button, value)
end)
Connections can also be subscribed by calling the connection object directly:
onClick(function(button, value)
print(button, value)
end)
Connection handle methods:
| Method | Description |
|---|---|
:Unconnect() |
Remove this specific subscription |
:Unconnect(handle)
Remove a subscription using its handle.
local handle = conn:Connect(myFunc)
-- later:
conn:Unconnect(handle)
:hasConnections()
Returns true if there is at least one subscriber.
:Lock(conn?) / :Unlock(conn?)
Lock/unlock the connection globally (blocking all fires) or per individual subscription handle.
conn:Lock() -- block all subscribers
conn:Unlock() -- unblock
Connection Operators
Connections support operator overloads for composing event pipelines.
+ — OR (merge two connections)
local merged = connA + connB
-- fires whenever either connA or connB fires
merged(function(...) print("Either fired", ...) end)
* — AND (requires all to fire)
local both = connA * connB
-- fires only after both connA AND connB have fired (then resets)
both(function(...) print("Both fired") end)
% — Map (transform values)
local mapped = transform_func % sourceConn
-- fires with transform_func applied to each emission
mapped(function(result) print(result) end)
/ — Filter (conditional forward)
local filtered = filter_func / sourceConn
-- fires only when filter_func returns truthy (first return value is the guard)
filtered(function(...) print("Passed filter", ...) end)
.. — Gate / Split
-- Gate: filter_func .. targetConn
-- Forwards to targetConn only when filter_func returns true
local gated = filter_func .. targetConn
-- Split: sourceConn .. sideEffect_func
-- Fires both sourceConn and sideEffect_func; returns modified conn
local split = sourceConn .. sideEffect_func
forwardConnection
Forwards all emissions from one connection to another:
multi.forwardConnection(source, destination)
Destroying Connections
conn:Destroy()
-- or
conn:destroy()
This:
- Removes all subscriptions
- Recursively destroys all child connections created by operators
- Nulls back-references
- Replaces methods with no-ops to prevent stale calls
Processors
A processor is an isolated scheduler with its own actor list and thread pool.
newProcessor
local proc = multi:newProcessor(name?, opts?, priority?)
Simple form:
local proc = multi:newProcessor("MyProc")
proc:Start()
With options table:
local proc = multi:newProcessor("MyProc", {
Start = false, -- start immediately?
Priority = nil, -- enable priority scheduling?
MaxThreads = -1, -- max concurrent threads (-1 = unlimited)
MaxObjects = -1, -- max actors (-1 = unlimited)
TaskDelay = 0, -- delay between task handler executions
Attach = false, -- attach as a loop to parent (auto-driven)
TaskHandler = true, -- create the built-in task handler thread
})
Processor Options
| Option | Type | Default | Description |
|---|---|---|---|
Start |
boolean | false | Whether to start the processor immediately |
Priority |
boolean | nil | Enable priority scheduling for this processor |
MaxThreads |
number | -1 | Thread cap (-1 = unlimited) |
MaxObjects |
number | -1 | Actor cap (-1 = unlimited) |
TaskDelay |
number/function | 0 | Delay between deferred task executions |
Attach |
boolean | false | If true, drives itself as a loop inside the parent |
TaskHandler |
boolean | true | Whether to spawn the built-in task handler thread |
Running a Processor
If Attach = false, you must drive the processor manually by calling proc.run(dt?) each frame, or inside a thread:
local proc = multi:newProcessor("Worker")
proc:Start()
thread:newThread("ProcDriver", function()
while true do
proc.run()
thread.yield()
end
end)
If Attach = true (default), a newLoop is created on the parent and drives the processor automatically.
local proc = multi:newProcessor("AutoProc", { Attach = true, Start = true })
-- No extra thread needed; proc runs as part of the parent loop
Key methods on a processor:
| Method | Description |
|---|---|
proc:Start() |
Activate the processor |
proc:Stop() |
Deactivate (still exists, just stops ticking) |
proc:Destroy() |
Destroy the processor and its attached loop |
proc:newThread(name, func, ...) |
Spawn a thread inside this processor |
proc:newFunction(func, holdme?) |
Create a TFunc inside this processor |
proc:getThreads() |
Returns the thread list |
proc:getHandler() |
Returns the internal coroutine handler |
proc:setMaxThreads(n) |
Change the thread cap at runtime |
proc:setMaxObjects(n) |
Change the object cap at runtime |
proc:boost(n) |
Run n handler passes per run() call |
proc:setTaskDelay(n) |
Change the task delay at runtime |
proc:isActive() |
Returns true if active |
proc:getFullName() |
Returns "parent.procName" |
Creating actors inside a processor:
All standard constructors work on processors:
proc:newLoop(func)
proc:newTLoop(func, interval)
proc:newAlarm(seconds, func)
-- etc.
Priority System
Priorities control how frequently an actor is run when priority = true is set in init() or on a processor.
Priority Constants
| Constant | Value | Description |
|---|---|---|
multi.Priority_Core |
1 | Runs every iteration (highest) |
multi.Priority_Very_High |
4 | Runs every 4 iterations |
multi.Priority_High |
16 | Runs every 16 iterations |
multi.Priority_Above_Normal |
64 | Runs every 64 iterations |
multi.Priority_Normal |
256 | Default |
multi.Priority_Below_Normal |
1024 | |
multi.Priority_Low |
4096 | |
multi.Priority_Very_Low |
16384 | |
multi.Priority_Idle |
65536 | Runs very rarely |
Setting Priority
actor:setPriority("normal") -- by name
actor:setPriority("high")
actor:setPriority("core")
actor:setPriority(multi.Priority_High) -- by constant
Accepted string shortcuts:
core / c, very high / vh, high / h, above / a, normal / n, below / b, low / l, very low / vl, idle / i
Resolving Priority
print(multi.PriorityResolve[multi.Priority_High]) -- "High"
Resetting Priority
actor:ResetPriority() -- restore to the value set at creation
Services
A service is a priority-managed background thread designed for long-running tasks.
local svc = multi:newService(function(self, data)
-- runs continuously while active
print("Service running, data:", data)
end)
svc.Start()
Methods:
| Method | Description |
|---|---|
svc.Start() |
Start the service |
svc.Stop() |
Stop and clear service data |
svc.Pause() |
Pause (freeze timer) |
svc.Resume() |
Resume |
svc.Destroy() |
Kill thread and stop |
svc.GetUpTime() |
Seconds since start |
svc:SetPriority(n) |
Set scheduling priority |
svc:SetScheme(n) |
Change the sleep/skip scheme (1, 2, or 3) |
Connections:
| Connection | Fires When |
|---|---|
OnStarted |
Service is started |
OnStopped |
Service is stopped |
OnError |
Service thread errors |
Schemes:
| Scheme | Behavior |
|---|---|
| 1 (default) | Uses thread.sleep with priority-derived delay |
| 2 | Uses thread.skip with priority-derived skip count |
| 3 | Not yet implemented time based scheme which bases on how long each loop of the service takes |
Tasks (Deferred Work)
Tasks are one-shot functions queued for execution by the processor's Task Handler thread.
multi:newTask(function()
print("I run asynchronously in the task queue")
end)
Or on a specific processor:
proc:newTask(function()
print("Task on proc")
end)
Tasks run in FIFO order. The built-in task handler thread processes them one at a time.
Configuring task delay:
multi:setTaskDelay(0.1) -- wait 0.1s between tasks
-- or a function:
multi:setTaskDelay(function() return someCondition() end)
Scheduled Jobs
Schedule a function to run when a specific time matches. Uses os.date patterns.
multi:scheduleJob(timeTable, func)
timeTable is a table of os.date("*t") fields. The job fires whenever all specified fields match the current time.
-- Run at 14:30:00 every day
multi:scheduleJob({ hour = 14, min = 30, sec = 0 }, function()
print("It's 2:30 PM!")
end)
The scheduler checks every second using an internal thread.
Global Variables & Thread Communication
Threads can communicate via a shared global variables table.
thread.set("myKey", someValue)
local v = thread.get("myKey")
thread.waitFor(name)
Blocks the current thread until a global variable is set.
thread:newThread("Consumer", function()
local value = thread.waitFor("resultReady")
print("Got:", value)
end)
thread:newThread("Producer", function()
thread.sleep(2)
thread.set("resultReady", 42)
end)
thread.pushStatus(...)
Fire the OnStatus connection of the current thread (or the connection thread that triggered this call).
thread:newThread("StatusPusher", function()
thread.sleep(1)
thread.pushStatus("halfway done")
thread.sleep(1)
return "final result"
end):OnStatus(function(msg)
print("Status:", msg)
end)
Type System
multi uses a simple string-based type registry.
Registering Types
local myType = multi.registerType("myObject", "myObjects")
-- Returns the type string "myObject"
-- multi.$MYOBJECT is set as a constant
Checking Types
obj:isType(multi.registerType("loop")) -- true if obj is a loop
multi.hasType("loop") -- returns the registered name or nil
multi.isMulitObj(obj) -- true if obj has a registered Type
Built-in Types
| Type String | Access Constant |
|---|---|
"rootprocess" |
multi.$ROOTPROCESS |
"process" |
multi.$PROCESS |
"loop" |
multi.$LOOP |
"tloop" |
multi.$TLOOP |
"alarm" |
multi.$ALARM |
"step" |
multi.$STEP |
"tstep" |
multi.$TSTEP |
"event" |
multi.$EVENT |
"updater" |
multi.$UPDATER |
"timer" |
multi.$TIMER |
"thread" |
multi.$THREAD |
"connector" |
multi.$CONNECTOR |
"service" |
multi.$SERVICE |
"function" |
multi.$FUNCTION |
"timemaster" |
multi.$TIMEMASTER |
Destroyed Objects
When an object is fully destroyed via multi.setType(obj, multi.DestroyedObj), all field accesses return a dead sentinel object that silently absorbs all operations.
if getmetatable(obj) == multi.DestroyedObj then
print("Object is destroyed")
end
Utility Functions
Logging
multi.print(...) -- prints INFO (only if settings.print = true)
multi.warn(...) -- prints WARNING (only if settings.warn = true)
multi.debug(...) -- prints DEBUG with traceback (only if settings.debugging = true)
multi.error(self?, msg) -- prints ERROR; hard-errors if settings.error = true
multi.success(...) -- always prints SUCCESS
All use ANSI color codes.
Math & Tables
multi.Round(num, decimalPlaces?) -- round to N decimal places
multi.AlignTable(tab) -- format a 2D table as aligned columns
table.merge(t1, t2) -- deep-merge t2 into t1, returns t1
Misc
multi.randomString(n) -- returns a random alphanumeric string of length n
multi.ForEach(tab, func) -- calls func(tab[i]) for each element
multi.timer(func, ...) -- runs func(...), returns elapsed time and return values
multi.isTimeout(val) -- true if val is a TIMEOUT sentinel
multi.isMulitObj(obj) -- true if obj has a registered multi type
os.getOS() -- returns "windows" or "unix"
os.sleep(n) -- blocking OS sleep (avoid; prefer thread.sleep)
Benchmarking
local bench = multi:benchMark(seconds, priority?, label?)
bench.OnBench(function(time, steps)
print("Completed", steps, "iterations in", time, "seconds")
end)
Getting Active Information
multi.getCurrentProcess() -- returns the currently executing processor
multi.getCurrentTask() -- returns the currently executing actor
multi:getChildren() -- returns the Mainloop array of this processor
multi:getRunners() -- returns non-internal actors in Mainloop
multi:getThreads() -- returns the thread list
multi:getProcessors() -- returns all registered sub-processors
multi:getStats() -- returns a stats table for all processors
Settings & Initialization
local multi, thread = require("multi"):init({
print = false, -- enable multi.print()
warn = false, -- enable multi.warn()
debugging = false, -- enable multi.debug() and debugManager
error = false, -- hard-error on runtime errors
priority = false, -- priority-based scheduling
findopt = false, -- anonymize function optimization hints
})
init() can be called multiple times but only applies settings on the first call. Subsequent calls just return multi and thread.
Default Settings
multi.defaultSettings -- table defining what the default settings are
System Events
These connections are available on the multi root object and fire at specific lifecycle points.
| Connection | Fires When |
|---|---|
multi.OnObjectCreated |
Any actor is created via :create() |
multi.OnObjectDestroyed |
Any actor is destroyed |
multi.OnLoad |
Just before the main loop starts (or on first thread creation) |
multi.OnPreLoad |
Just before each uManager pass |
multi.OnExit |
When os.exit() is called |
multi.OnError |
Global error handler |
multi.enableOptimization |
When optimization mode is enabled |
multi.settingsHook |
When init() is called with settings |
multi.OnObjectCreated(function(obj, parent)
print("Created:", obj.Type, "on", parent.Name)
end)
multi.OnExit(function(code)
print("Exiting with code:", code)
end)
UUID Utilities
multi includes a UUID v7 generator (timestamp-based).
local uuid = multi.generate_uuid7()
-- e.g. "018fdb62-1a00-7abc-8def-012345678901"
Extracting Timestamps
local info = multi.extract_uuid7_timestamp(uuid)
-- info.milliseconds -- Unix timestamp in ms
-- info.seconds -- Unix timestamp in seconds
-- info.date -- "YYYY-MM-DD HH:MM:SS"
-- info.iso8601 -- "YYYY-MM-DDTHH:MM:SS.mmmZ"
Object Creation Timestamps
Every actor created via :create() receives a UID. The creation timestamp can be retrieved:
local ts = actor:GetCreationTimestamp()
-- returns an ISO 8601 string
Advanced Patterns
Chaining with SetTime / ResolveTimer
SetTime adds a timeout to any actor. If the actor doesn't call ResolveTimer within the specified duration, OnTimedOut fires and the actor is paused.
local myLoop = multi:newLoop(function(self, t)
if someCondition() then
self:ResolveTimer("success")
end
end)
myLoop:SetTime(5)
myLoop.OnTimedOut(function(self)
print("Timed out after 5 seconds!")
end)
myLoop.OnTimerResolved(function(self, reason)
print("Resolved with:", reason)
end)
Reallocating Actors Between Processors
An actor can be moved from one processor to another at runtime.
local proc1 = multi:newProcessor("P1"):Start()
local proc2 = multi:newProcessor("P2"):Start()
local loop = proc1:newLoop(function() print("running") end)
-- Move to proc2:
loop:reallocate(proc2)
multi.hold() Outside Threads
multi.hold() can be called from outside a thread. It spins the main loop internally until the condition resolves.
-- Block main script execution until a connection fires:
local result = multi.hold(someConnection)
This is useful for top-level async patterns before mainloop() is started.
thread.defer
Registers a function to run when the current thread dies or errors.
thread:newThread("WithCleanup", function()
thread.defer(function(th)
print("Thread died, cleaning up")
end)
-- do work ...
end)
thread.chain
Runs a sequence of hold conditions one after another.
thread:newThread("Sequencer", function()
thread.chain(
function() return conditionA() end,
connB,
function() return conditionC() end
)
print("All three conditions satisfied in order")
end)
Optimization Detection
When findopt = true is passed to init(), multi detects anonymous functions passed repeatedly to thread.hold and emits a warning.
multi.optConn(function(msg)
print("OPT HINT:", msg)
end)
Quick Reference Card
ACTORS (created on multi or processor)
multi:newLoop(func?) -- fires every iteration
multi:newTLoop(func?, interval?) -- fires every N seconds
multi:newAlarm(secs?, func?) -- one-shot after N seconds
multi:newStep(s,e,c?,skip?) -- counter s→e by c
multi:newTStep(s,e,c?,interval?) -- timed counter
multi:newEvent(task?, func?) -- fires when task() is truthy
multi:newUpdater(skip?, func?) -- fires every N iterations
multi:newTimer() -- utility timer (not an actor)
ALL ACTORS SHARE
:Pause() :Resume() :Destroy()
:setPriority(s) :setName(s)
:isPaused() :isActive() :isDone()
OnBreak OnPriorityChanged
CONNECTIONS
conn = obj:newConnection(protect?, func?, kill?)
conn:Fire(...) -- emit
conn:Connect(func) -- subscribe → handle
conn(func) -- shorthand Connect
handle:Unconnect() -- unsubscribe
conn:Destroy() -- teardown
conn + conn2 -- OR merge
conn * conn2 -- AND gate
func % conn -- map transform
func / conn -- filter
func .. conn -- gate/split
THREADS
thread:newThread(name?, func, ...)
thread:newFunction(func, holdme?)
thread:newISOThread(name?, func, env?, ...)
-- inside threads:
thread.sleep(n)
thread.hold(cond, opts?)
thread.yield()
thread.skip(n)
thread.holdFor(secs, cond?)
thread.holdWithin(cycles, cond?)
thread.set(k,v) / thread.get(k) / thread.waitFor(k)
thread.pushStatus(...)
thread.isThread()
PROCESSORS
proc = multi:newProcessor(name?, opts?)
proc:Start() / proc:Stop() / proc:Destroy()
proc.run(dt?) -- manual tick
proc:newThread(...) -- spawn thread inside proc
proc:setMaxThreads(n)
proc:boost(n)
PRIORITIES
"core"/"c" "very high"/"vh" "high"/"h"
"above"/"a" "normal"/"n" "below"/"b"
"low"/"l" "very low"/"vl" "idle"/"i"
UTILITIES
multi.isTimeout(v)
multi.Round(n, dec?)
multi.randomString(n)
multi.timer(func, ...)
multi:benchMark(secs)
multi.generate_uuid7()
multi.extract_uuid7_timestamp(uuid)
LOGGING (controlled by init settings)
multi.print(...) -- INFO (blue)
multi.warn(...) -- WARNING (yellow)
multi.debug(...) -- DEBUG (white)
multi.error(...) -- ERROR (red)
multi.success(...) -- SUCCESS (green)
Documentation generated for multi v16.3.0-testing.