Can race conditions happen in Luau without yielding?

Hello, I have a question about mutexes. Should they only be used when a function can yield or wait?

For example, in this case, a mutex is not needed, right?

function ProgressionService:AddXp(player: Player, amount: number)
	assert(typeof(amount) == "number", string.format(
		"[AddXp] Invalid amount type. Expected number, got %s (%s)",
		typeof(amount),
		tostring(amount)
		))
	
	if amount <= 0 then
		return false, "Amount must be greater than 0"
	end
	
	local profile = DataManager:GetProfile(player)
	if not profile then
		return false, "Player profile is not loaded."
	end
	
	local replica = DataManager:GetReplica(profile)
	if not replica then
		return false, "Player data is not ready yet."
	end
	
	local data = profile.Data
	local stats = data.Stats

	local level = stats.Level
	local oldLevel = level
	local xp = stats.Xp
	local rebirth = stats.Rebirth
	local maxLevel = Utils.GetMaxLevelByRebirth(rebirth)

	if level >= maxLevel then
		return false, "Player is already at the maximum level."
	end

	xp += amount

	-- Process Xp
	while level < maxLevel do
		local xpRequired = Utils.GetXpForLevel(level + 1)

		if xp < xpRequired then
			break
		end

		xp -= xpRequired
		level += 1
	end

	if level >= maxLevel then
		xp = 0
	end

	replica:Set({"Stats", "Xp"}, xp)

	if level > oldLevel then
		replica:Set({"Stats", "Level"}, level)

		if not player:GetAttribute("CustomSpeed") then
			CustomSpeedService:ApplySpeedModifier(player)
		end
	end

	return true
end

Since Lua is single-threaded, even if 10 different scripts call this function at the same time, they will be executed sequentially in a queue, so there shouldn’t be any race conditions or incorrect data updates. Is that correct?

Also, since I have this method inside ProgressionService , should every other script use this method instead of directly calling Replica:Set ? What could happen if another script modifies the replica data directly using Replica:Set instead?

1 Like

You are correct, you don’t need any sort of thread syncing devices like mutexes. Luau only allows one thread to run at a time. So if your thread is running, no other Luau thread is running in the entire game (kinda…we have parallel Luau with Actors, but that’s a separate topic & mutexes are still not needed due to VM isolation).

However, you can still get into a desync with asynchronous calls. Take this for example:

local data = { num = 10 }

function incrementNum()
   local value = data.num
   task.wait(math.random())
   data.num = value + 1
end

for i = 1, 10 do
   task.spawn(incrementNum)
end

task.wait(1.5)
print(data.num) -- This will print '11'

The above code all grabs ‘num’ when it’s 10, then adds 1 after a random wait. So even though the increment function is run 10 times, it acts as if it was run once. The point being: if your AddXp code might cause a yield between the time you retrieve data and when you change it, that could be a source of issues similar to what you see in multithreading. However, it doesn’t look like your AddXp code has any yields (but I don’t know what the various function calls do).


Mutexes are needed in multithreading because the OS scheduler can decide to pause a thread at any given time and move onto another one, which is problematic when multiple threads interact with the same memory. And this could happen in the middle of an operation (e.g. x += 1 traditionally takes more than one CPU instruction, so you might get caught in the middle of that logical piece of code, while another thread then tries to read/write to x too).

4 Likes

Im not sure why you said it like that, the fact that Lua is single threaded means 10 different scripts cannot call it at the exact same time. Lua doesnt store anything like this in a queue. If you meant on the same frame, then yes, in a sense each script is a coroutine(managed by roblox’s system) and somehow, in some order, the 10 scripts will call this function 10 times, but there may be other threads that resume in between these. I suppose the only thing in a queue would be the coroutines themselves, waiting to be resumed.

Also the one time I really had an issue with something like a ‘data-race’ was a remote event or remote function invocation that reads data, yields, and then writes data. What would happen is that players on the client would spam the remote event/function and cause some weird exploits. The code itself looked harmless, yet poof bugs/exploits occur. In this case, I had to use a per-player debounce until the previous event handlers completed. In that sense, I suppose it’s something like a mutex

1 Like

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