After resuming a Wait() thread, wait() no longer functions properly.
Code:
delay(4, function()
coroutine.resume(thread, true);
end)
while true do
thread = coroutine.running();
print("New");
local nt = tick();
if true then
local r = wait(1);
if r == true then
print("Resume A");
else
print("Skip A", r);
end
end
local r2 = wait(2);
if r2 == true then
print("Resume B");
else
print("Skip B", r2, tick()-nt);
end
end
Output:
New
Skip A 1.0116717000019
Skip B 2.0001374999993 3.0119519233704
New
Resume A
Skip B 1.0012400000014 1.0012452602386
New
Skip A 1.0142976999996
Skip B 2.0144725999999 2.0143115520477
New
Skip A 2.018734199999
Skip B 1.0188027000004 1.0188076496124
First cycle is fine, A yielded for 1 second, B yielded for 2, total of 3 seconds.
Second cycle, A’s yield was resumed, B then yields for only 1 second when it should had yielded for 2.
Third cycle, A yields for 1 second, but B says it yielded for 2 seconds but the time only lapsed for 2 seconds shown by the tick() check.
Fourth cycle, even more bizarre.
Note: I’ve replace wait() with a BindableEvent and it is working like it should.
You are resuming a thread which is being handled by the Roblox scheduler. The initial call to wait will queue up a resume task to that thread which will be dispatched after the specified number of seconds pass. In your case, the resume task queued by the second call to A is resuming the second call to B, and from there you have this knock-on effect.
The following code is a corresponding Lua implementation of the wait function.
function wait(duration)
local thread = coroutine.running()
delay(duration, function (...)
coroutine.resume(thread, ...)
end)
return coroutine.yield()
end
Roblox’s scheduler uses the same mechanism to yield threads as coroutines do, which is why you’re able to yield and resume them at your leisure.
The simple answer is that you should only resume threads that you’ve yielded. If you want some other behavior, then you can always implement that yourself.
Interesting, I’ve also rewrote a wait() of my own and I had a check inside the delay in order to cancel the resume if the yield has already been resumed externally.
function wait(duration)
local thread = coroutine.running()
local yielded = false
delay(duration, function (...)
if yielded then return end;
coroutine.resume(thread, ...)
end)
local returns = coroutine.yield()
yielded = true
return returns
end
I think that’s what causing the wait() to resume earlier than expected. Why doesn’t Roblox has a check for the delay?
Roblox’s engine is not written in Lua. The example code I gave simulates the behavior of wait but it is not a 1:1 translation of the actual implementation. When yielding a Lua thread from C you call the lua_yield method on the current lua_state. While the Lua, which was executing, yields, the engine continues to run other tasks (it does not yield).
What this means is that the example you gave is a lot more complex to implement than it seems. It would require an additional data structure which tracks why a thread was yielded. This creates overhead and then the question becomes whether or not making yields/resumes slightly slower is worth it to implement the behavior you’re describing.
Unfortunately not yet. I came up with a solution to the problem but it’s not performant enough. The intention is to see if there’s a better solution. Until such a time, I’d recommend using bindables.
You are correct, there is a different mechanism responsible for resuming threads in Roblox’s scheduler. Eventually, it goes through the same method as coroutine.resume but not before doing some extra operations.