Async | Advanced Parallel Luau

async

Async lets you create a pool of Actors, schedule work against a ModuleScript function, and collect the results, without writing the Actor and messaging boilerplate yourself.

It works well for compute-heavy jobs, such as running large batches of raycasts in parallel and returning the final data.

Get it here:
async.rbxm (3.6 KB)
https://create.roblox.com/store/asset/108534780317365/async

If you get sandbox errors, turn on experimental sandboxing in workspace and disable these checks under async’s scripts


Getting Started

local async = require(path)
local threads = async.new(threadCount: number, module: ModuleScript, functionName: string)
Parameter Type Description
threadCount number Number of parallel workers to create.
module ModuleScript The module containing the work to run in parallel.
functionName string The function in module that each job executes.

Whatever your function returns is passed back to you as a result.


Basic Usage

1. Create the parallel module

local module = {}

function module.foo(str: string)
	return str
end

return module

2. Create a registry

A registry is a named buffer that stores the results of your scheduled work. The name can be any string, instance or value. You can create multiple registries and read each one independently.

threads:initBuf("r1")

3. Schedule work

The first argument is the registry name. All remaining arguments are passed to your module function.

threads:schedule("r1", "hello world!")

4. Execute

After scheduling all of your jobs, call collect. It dispatches the work to the workers and yields until everything has finished.

threads:collect()

5. Read the results

local results: { any } = threads:read("r1")
print(results)

-- Output:
-- {
--     [1] = "hello world!"
-- }

Results are always returned as an array. Each call to schedule adds one entry:

{
	[1] = "hello world!",
	[2] = "hello world!",
	-- ...
	[n] = "hello world!",
}

Fire-and-Forget Work

If you don’t need return values, use call. It runs the work in parallel without storing results and is roughly 30% faster than schedule.

threads:call(...)

Memory Management

Release your registries when you are done with them.

threads:reset()          -- Clears and removes all registries
threads:clearBuf("r1")   -- Clears the contents of a specific registry
threads:removeBuf("r1")  -- Removes a specific registry from memory

Example code

local module = {}
local nbuf = require(script.nbuf)

function module.raycast(buf: buffer) : buffer
	
	local position = nbuf.read4vec(buf, 0)
	local direction = nbuf.read4vec(buf, 12)
	
	return workspace:Raycast(position, direction)
	
end

return module

const async = require(game.ReplicatedStorage.async)
const threads = async.new(24, script.ModuleScript, 'raycast')
local nbuf = require(script.nbuf)

threads:initBuf('rays')

local position = vector.create(9, 12, -92)
local direction = vector.zero - position
local buf = nbuf.write4vec({position, direction})

-- send work in parallel
threads:schedule('rays', buf)

-- work
threads:collect()

-- read
print(
	threads:read('rays')
)

-- clear
threads:reset()

expected result:

{
   [1] = RaycastResult{SpawnLocation @ 0.586956024, 0.782608032, -6; normal = 0, 0, -1; material = Plastic}
}
note: it can return a blank table if no raycast has been found.

Best Practices

1. Use buffers for input and output and send more data

Buffers are much faster than tables when sending data to, or returning data from, parallel code. For example, instead of sending a table of vectors:

{ vector, vector, vector, ... }

pack them into a single buffer and decode it inside the worker. Each vector takes 12 bytes (3 × f32), so the buffer size is vectorCount * 12.

local buf = buffer.create(12)
buffer.writef32(buf, 0, x)
buffer.writef32(buf, 4, y)
buffer.writef32(buf, 8, z)

If packing, reading, and combining buffers by hand sounds tedious, I wrote a small helper module that handles it: nbuf.luau (877 Bytes)

2. Distribute work evenly

Split your workload evenly across the workers you created with async.new(threadCount, ...). If one Actor gets overloaded while others sit idle, you lose much of the benefit of parallelism.


Feedback, bug reports, and suggestions are welcome in the replies.
Note: I wrote the module and the original documentation myself, then used AI to polish the wording and formatting of this post.

9 Likes

I literally started writing the EXACT same thing like 3 hours ago, but mine uses shared tables instead of buffers and has a slightly different API. Truly a parallel discovery moment
Edit: Released

5 Likes