Parallel Lua also slows down the main thread?

I am creating a procedural generation system and am trying to use actors to optimize performance (there are currently large lag spikes when generating levels)

I currently create 3 “Worker” actors which run generation code:

local function buildCave(jobId, payload)
	local sharedResultKey = payload.sharedResultKey
	local success, tileGrid, featureGrid, wallFeatureGrid = pcall(CaveBuilder.build, payload.depth, payload.integrity, {
		config = mineConfig,
		seed = payload.seed,
	})

	if success then
		local gridWidth, gridHeight = getGridSize(tileGrid)
		local sharedResult = SharedTable.new()
		local sharedTileGrid = toSharedValue(tileGrid)

		sharedTileGrid._gridWidth = gridWidth
		sharedTileGrid._gridHeight = gridHeight

		sharedResult.jobId = jobId
		sharedResult.tileGrid = sharedTileGrid
		sharedResult.featureGrid = toSharedValue(featureGrid)
		sharedResult.wallFeatureGrid = toSharedValue(wallFeatureGrid)
		SharedTableRegistry:SetSharedTable(sharedResultKey, sharedResult)

		resultEvent:Fire({
		jobId = jobId,
		success = true,
		sharedResultKey = sharedResultKey,
	})
	else
		resultEvent:Fire({
			jobId = jobId,
			success = false,
			error = tostring(tileGrid),
		})
	end
end

actor:BindToMessageParallel("GenerateFloor", buildCave)

(toSharedValue is basically just a deep recursive table clone)

I clone this script into 3 actors and manage them with a task scheduler and I use BindableEvents to communicate with the scheduler.

The game keeps a stack of 3 layers, generating and removing them as the player descends floors.
When a layer is generated, a server lag spike occurs, similar as if it was generated on the main thread. This really shouldn’t occur for true isolated threads, since the code does not await for worker threads to finish anywhere. The map renderer does clone a lot of instances, but I have it call task.wait() after every 5 tiles. (i % 5 == 0)

That’s when I found out that actors aren’t true isolated threads.

From my experiments with them, actors really only guarantee parallelism as they can still slow down the main serial thread.

Mine generation is only faster when generating multiple floors at once, which isn’t what my generator is built for. It generates large, expensive floors once every few minutes, except for the start of the game when the layer stack is built.


Here’s another test I did on a simple worker/coordinator setup. (This time with the coordinator script in its own actor)

Coordinator

local ServerScriptService = game:GetService("ServerScriptService")
local worker = ServerScriptService.W1
local actor = script:GetActor()

actor:BindToMessage("Done", function()
	print("Worker done!")
end)

while true do
	task.wait(5)
	worker:SendMessage("Work")	
end

Worker

local actor = script:GetActor()
local coordinator = game:GetService("ServerScriptService").Coordinator

actor:BindToMessageParallel("Work", function()
	local v = 0
	for i = 1, 1_000_000_000 do
		v += i
	end
	coordinator:SendMessage("Done")
end)

This also pauses the main server thread until the task is complete.

Is there a way to avoid this issue, having parallel code not also run on/block the serial thread? This feature really wouldn’t be much help for my game otherwise.

The main thread waits for actors to complete (finish or yield) each frame. Long-running actors will stretch the current frame time. You can learn more about this under the Best Practices section of the Parallel Luau article.

In other words, while actors run on separate threads, they are not decoupled from the frame in which they run. It’s best to see actors as a way to run parallel tasks, not necessarily just multithreaded code. That’s a nuanced but important logical difference in how you approach a problem.

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.