Acquis 11 - Workers

Workers in Lus allows for concurrency through the instantiation of separate interpreter states running in their own threads. While workers appear to the user as isolated execution contexts, the underlying implementation utilizes a thread pool (M:N) architecture.

Rather than mapping one worker strictly to one operating system thread, Lus utilizes a fixed number of OS threads (M) to service a potentially larger number of worker states (N). The Lus runtime creates a pool of OS threads upon initialization (typically equal to the number of logical CPU cores), and worker states are scheduled onto these threads dynamically.

Basic usage

Workers can be created by passing a filepath and optional arguments to the worker.create function:

local p = worker.create("script.lus")

-- Worker with arguments.
local w = worker.create("script.lus", { a = 1 }, "foobar")

Arguments passed to a worker will be copied to the new interpreter state and are accessible in varargs (...). Userdata and threads are not copied to the new state and attempts to pass any will error before the worker is created.

The current worker state can be retrieved with worker.status, similar to coroutine.status, which can either return running when a worker is still running or dead when the worker has returned.

Message passing

Communication between the main thread and Workers is asynchronous and bi-directional. Messages are deep-copied between states to ensure thread safety.

Worker to Main (message and receive)

Workers can send messages to their parent state with the worker.message global function. The parent state receives these by passing the worker instance(s) to worker.receive.

script.lus

global worker

-- Send data back to parent
worker.message("Process started")
for i = 1, 10 do
  worker.message(i)
end

main.lus

global worker

local p = worker.create("script.lus")

-- Blocks until a message is available or the worker dies
while result = worker.receive(p) do
  print("Got result:", result)
end

Select-like Behavior: worker.receive accepts multiple workers as arguments. It behaves like a select operation: it blocks until at least one of the provided workers has a message available. It does not wait for all workers.

The return values correspond to the order of the arguments passed. If a worker has no message ready, its corresponding return value is nil.

local p1 = worker.create("slow_task.lus")
local p2 = worker.create("fast_task.lus")

while true do
  -- Block until either p1 OR p2 sends a message
  local res1, res2 = worker.receive(p1, p2)

  if res1 then
    print("p1 says:", res1)
  end

  if res2 then
    print("p2 says:", res2)
  end

  -- Break if both workers are dead and empty
  if worker.status(p1) == "dead" and worker.status(p2) == "dead"
     and not res1 and not res2 then
     break
  end
end

Main to Worker (send and peek)

The parent state can send messages to a running worker using worker.send. The worker consumes these messages using worker.peek.

main.lus

local w = worker.create("processor.lus")

-- Send instructions to the running worker
worker.send(w, "START")
worker.send(w, { data = {1, 2, 3} })

processor.lus

global worker

while true do
  -- worker.peek() blocks until the parent sends a message
  local msg = worker.peek()

  if msg == "START" then
    print("Starting...")
  elseif type(msg) == "table" then
    process_data(msg.data)
  elseif msg == "STOP" then
    break
  end
end

Like receive, worker.peek queues messages. If multiple messages are sent before peek is called, they are stored in the state’s inbox. If worker.peek is called when the queue is empty, it blocks until a message arrives.

Error handling

Errors in workers will also queue and will be thrown in worker.receive. The following pattern is recommended if you are expecting workers to error:

while err, result = catch worker.receive(p) do
  if err then
    print("Worker crashed:", err)
    break
  end
  -- process result
end

Permissions

Usage of workers requires the load and the requisite fs:read pledges like require.

Workers will inherit the parent’s permissions, but can be restricted further by invoking pledge in the worker state.