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?
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).
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