Files
multi/docs/multi_library_documentation.md
2026-04-04 01:00:50 -07:00

37 KiB

multi — Lua Cooperative Multitasking Library

Version 16.3.0 · MIT License · by Ryan Ward


Table of Contents

  1. Overview
  2. Core Concepts
  3. Getting Started
  4. The Main Loop
  5. Task Types (Actors)
  6. Threads
  7. Connections (Events)
  8. Processors
  9. Priority System
  10. Services
  11. Tasks (Deferred Work)
  12. Scheduled Jobs
  13. Global Variables & Thread Communication
  14. Type System
  15. Utility Functions
  16. Settings & Initialization
  17. System Events
  18. UUID Utilities
  19. Advanced Patterns
  20. 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.sleep and thread.hold yield 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 object
  • elapsed — seconds since the loop was created (or nil if notime = 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 times OnLoop has 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.