After resuming a Wait() thread, wait() no longer functions properly

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
  1. First cycle is fine, A yielded for 1 second, B yielded for 2, total of 3 seconds.
  2. Second cycle, A’s yield was resumed, B then yields for only 1 second when it should had yielded for 2.
  3. 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.
  4. Fourth cycle, even more bizarre.

Note: I’ve replace wait() with a BindableEvent and it is working like it should.

Related?

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.

Ah I see, thank you for the clarification.

I hope you don’t mind if I ask it here, but has this issue been resolved?

Roblox has their own thread scheduler so it may be more complex than just the Lua C api calls. Perhaps wait and coroutine treat it differently.

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.

I went into more detail about this on another thread ModuleScripts that yield with coroutine.yield() will break parent thread - #8 by woot3


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.

1 Like