On this page
  1. tecs.workers
  2. Polling and shutdown
  3. Module contents
    1. Constructors
    2. Types
    3. Functions
    4. Values
  4. Constructors
    1. newChannel
  5. Types
    1. Channel
    2. Self
    3. SpawnOptions
    4. Worker
  6. Functions
    1. current
    2. spawn
  7. Values
    1. path

tecs.workers

Worker threads and channels.

A worker is source text and two queues. It shares nothing with the state that spawned it, so it reaches its own ends through workers.current, and spawn pairs with stop:

local worker <const> = tecs.workers.spawn({
    source = [[
    local tecs <const> = require("tecs")
    local self <const> = tecs.workers.current()
    while true do
        local job = self:receive()
        if job == nil then break end
        self:send({name = job.name, hash = tecs.data.fnv1a64(job.bytes)})
    end
]],
})

worker:send({name = "level1", bytes = "..."})
local answer <const> = worker:receive(1000)
worker:stop()

The worker's source writes a require line because it is a separate state with a separate global table, which is the one place in a game that does.

This is the only sanctioned way to run work off the main thread. Raw thread creation is deliberately not exposed: a LuaJIT FFI callback invoked from a thread the VM did not create is unsafe, and a thread entry point written in Lua is exactly that mistake.

LuaJIT has no shared mutable heap across threads, so a worker cannot see the spawning state's objects at all. Values cross as serialized bytes, encoded here with string.buffer.

What can cross is therefore what string.buffer can encode: numbers, strings, booleans, and tables of those. Not functions, not userdata, and not cdata pointers into another state's heap.

Polling and shutdown

The spawner's receive polls by default, which suits a frame. Inside the worker it waits by default, so an idle worker does not spin. Closing the inbox wakes the worker and returns nil after queued messages drain.

stop closes the inbox, joins the thread, and releases both channels. Worker source must leave its receive loop when the inbox closes or shutdown can wait indefinitely.

Set TECS_TRACEPROF to print a worker's trace aborts when its inbox closes. Repeated failed to allocate mcode memory messages identify LuaJIT machine-code allocation failures.

Module contents

Constructors

Constructor Description
newChannel Creates an independent channel.

Types

Type Kind Description
Channel record Carries serialized messages between two states.
Self record Contains the worker's two channel endpoints returned by workers.current.
SpawnOptions record Options for workers.spawn.
Worker record Represents a thread with its own Lua state and two message channels.

Functions

Function Kind Description
current Static Returns the channels for the current worker.
spawn Static Starts a worker running options.source on its own thread and state.

Values

Value Type Description
path string Read-only. Contains the absolute path of the loaded native library.

Constructors

tecs.workers.newChannel Static

Creates an independent channel.

function tecs.workers.newChannel(): Channel

Arguments

None.

Returns

Type Description
Channel An owning handle. destroy releases the native queue, so exactly one state should hold it. Raises when the queue cannot be created.

Types

tecs.workers.Channel record

Carries serialized messages between two states.

The channel copies every value across and never shares it, so the receiver gets a separate object and mutating either one is invisible to the other. A nil is encodable and does cross, but receive also answers nil for "nothing here", so nil is not usable as a message a reader can recognize. Read-only. Exposes the Channel type.

record tecs.workers.Channel is Closeable
    handle: loader.CPtr

    wrap: function(handle: loader.CPtr): Channel
    count: function(self): integer
    destroy: function(self)
    receive: function(self, timeoutMs: number): any
    send: function(self, value: any)
end

Interfaces

Interface
Closeable

tecs.workers.Channel.handle field

Engine-owned. Stores the native queue pointer until destroy runs. Ordinary game code should ignore this field.

tecs.workers.Channel.handle: loader.CPtr

tecs.workers.Channel.wrap Static

Wraps a channel pointer handed over by the native side.

function tecs.workers.Channel.wrap(handle: loader.CPtr): Channel
Arguments
Name Type Description
handle loader.CPtr The native side keeps this pointer alive for the wrapper's lifetime.
Returns
Type Description
Channel A borrowing handle. destroy does nothing, so the state that created the queue remains responsible for releasing it.

tecs.workers.Channel:count Instance

function tecs.workers.Channel.count(self): integer
Arguments
Name Type Description
self Channel
Returns
Type Description
integer

tecs.workers.Channel:destroy Instance

function tecs.workers.Channel.destroy(self)
Arguments
Name Type Description
self Channel
Returns

None.

tecs.workers.Channel:receive Instance

Takes the next value, or nil if none arrived.

A zero timeout polls, a negative timeout waits indefinitely, and any other value waits that many milliseconds.

function tecs.workers.Channel.receive(self, timeoutMs: number): any
Arguments
Name Type Description
self Channel
timeoutMs number The timeout uses milliseconds. Omit it to poll, which is the opposite of the worker-side receive, where omission waits.
Returns
Type Description
any The next value, or nil. Nil covers an empty queue, a timeout, and a closed and drained channel. A sent nil also decodes to nil, so callers cannot distinguish those cases and should not send nil.

tecs.workers.Channel:send Instance

Serializes value and queues it without blocking.

The queue has no bound, so a reader that stops taking messages grows it instead of pushing back on the writer.

function tecs.workers.Channel.send(self, value: any)
Arguments
Name Type Description
self Channel
value any The channel copies this value before returning. A function, userdata, cdata, a table nested past 32 levels, or a table with a key of any of those raises and names the path to it.
Returns

None.

tecs.workers.Self record

Contains the worker's two channel endpoints returned by workers.current.

global record tecs.workers.Self
    receive: function(self, timeoutMs: number): any
    send: function(self, value: any)
end

tecs.workers.Self:receive Instance

Takes the next task, or nil when the inbox closes.

function tecs.workers.Self.receive(self, timeoutMs: number): any
Arguments
Name Type Description
self Self
timeoutMs number Milliseconds. Omitted waits indefinitely, which is the opposite of Channel:receive; zero polls. Only the waiting form's nil means the spawner has asked this worker to stop, since a poll answers nil for a merely empty queue.
Returns
Type Description
any The next task, decoded into this state's own tables, so it shares nothing with what the spawner sent and mutating it is safe. Nil is the stop signal or an empty queue, on the terms above.

tecs.workers.Self:send Instance

Returns a result to the spawner.

function tecs.workers.Self.send(self, value: any)
Arguments
Name Type Description
self Self
value any Subject to the same limits as Channel:send, and raises the same way. Nothing bounds the queue, so a worker outrunning its spawner grows it.
Returns

None.

tecs.workers.SpawnOptions record

Options for workers.spawn.

global record tecs.workers.SpawnOptions
    source: string
    luaPath: string
end

tecs.workers.SpawnOptions.source field

Caller-writable. Sets the Lua source the worker runs. It reaches its channels through workers.current(). Required; nil raises. Source text, not a path, and it runs in a state that shares nothing with this one, so it cannot close over anything here.

tecs.workers.SpawnOptions.source: string

tecs.workers.SpawnOptions.luaPath field

Caller-writable. Sets package.path for the worker's state. Defaults to this state's, so a worker resolves the same modules the spawner does.

tecs.workers.SpawnOptions.luaPath: string

tecs.workers.Worker record

Represents a thread with its own Lua state and two message channels.

The worker shares no globals, upvalues or loaded modules with the spawning state. The worker reruns its own requires against luaPath. Read-only. Exposes the Worker type.

record tecs.workers.Worker
    pending: integer

    available: function(self): integer
    receive: function(self, timeoutMs: number): any
    send: function(self, value: any)
    stop: function(self): integer
end

tecs.workers.Worker.pending field

Read-only. Reports the messages the worker had not taken after the last send. The value does not refresh between sends.

tecs.workers.Worker.pending: integer

tecs.workers.Worker:available Instance

function tecs.workers.Worker.available(self): integer
Arguments
Name Type Description
self Worker
Returns
Type Description
integer

tecs.workers.Worker:receive Instance

Takes a result, or nil if none is ready.

function tecs.workers.Worker.receive(self, timeoutMs: number): any
Arguments
Name Type Description
self Worker
timeoutMs number The timeout uses milliseconds. Omit it to poll and return immediately; a negative value blocks until something arrives and stalls the frame when called from the main thread.
Returns
Type Description
any Nil means the queue is empty or the worker has exited. Call stop to learn whether the worker still runs.

tecs.workers.Worker:send Instance

Queues a value for the worker and refreshes pending.

function tecs.workers.Worker.send(self, value: any)
Arguments
Name Type Description
self Worker
value any The worker applies the same limits and errors as Channel:send.
Returns

None.

tecs.workers.Worker:stop Instance

function tecs.workers.Worker.stop(self): integer
Arguments
Name Type Description
self Worker
Returns
Type Description
integer

Functions

tecs.workers.current Static

Returns the channels for the current worker.

Only valid in a worker's state, where the native entry point installed the pointers as globals before running the source.

function tecs.workers.current(): Self

Arguments

None.

Returns

Type Description
Self A fresh object each call, borrowing channels the spawner owns. Call it once and keep the result: a second call makes a second pair of wrappers over the same two queues, and under TECS_TRACEPROF a second trace session, which raises.

tecs.workers.spawn Static

Starts a worker running options.source on its own thread and state.

Returns as soon as the thread starts, not when the source has run, so a failure inside the source surfaces as stop's status rather than here.

function tecs.workers.spawn(options: SpawnOptions): Worker

Arguments

Name Type Description
options SpawnOptions

Returns

Type Description
Worker A worker the caller must stop. Collection does not join the thread or release its channels.

Values

tecs.workers.path variable

Read-only. Contains the absolute path of the loaded native library.

tecs.workers.path: string