On this page
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(): ChannelArguments
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)
endInterfaces
| 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.wrap Static
Wraps a channel pointer handed over by the native side.
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): integerArguments
| 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): anyArguments
| 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)
endtecs.workers.Self:receive Instance
Takes the next task, or nil when the inbox closes.
function tecs.workers.Self.receive(self, timeoutMs: number): anyArguments
| 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.
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: stringtecs.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: stringtecs.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
endtecs.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:available Instance
function tecs.workers.Worker.available(self): integerArguments
| 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): anyArguments
| 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): integerArguments
| 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(): SelfArguments
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): WorkerArguments
| 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