Coroutine.yieldwait(thread)

Your code could be simplified to the following in order to function in vanilla Lua.

local t
local function Wait()
  local t = coroutine.running()
  return coroutine.yield()
end

spawn(function ()
   local secret = Wait()
   print(secret)
end)

wait(2)
coroutine.resume(t, "Hi")

Unfortunately, the Roblox scheduler will attempt to resume certain threads automatically. As such yield behaves as wait in many places. I spent some time during summer looking at just this problem, I believe I found the problem and implemented a fix but somebody else would need to take this further.

3 Likes

This feature request makes a lot of sense, but I instead decided to design my game in a functional signal-based way that avoids yielding and spawn/coroutine.wrap all-together. You might want to try the same thing. If delays are needed I use a custom scheduler.

I haven’t had any issues thus far with this. Upon creation of a new thread and coroutine.yield()ng in it, the thread will yield appropriately and with a reference to it via coroutine.running I can resume it fine.

EDIT: On a side note, coroutine.kill(thread) would be great!

coroutine.yield being broken was intentional? There’s no way to yield for other threads without yielding for the next frame now. Why would anyone want coroutine.yield to behave exactly like wait()?

3 Likes

I’m not entirely sure which I why I was looking into it; @zeuxcg might better be suited to answer this.

Here’s the thread I posted about that last month

>_<

A useful feature was removed because it didn’t match documentation. Why not just update documentation?

coroutine.yield pushing the thread to the back of the stack is important for timing certain things. For example if the parent of an object changes and you want to immediately change it to something else, you now have to wait an arbitrarily long amount of time (however long a frame takes) before you can perform that action. With coroutine.yield in working order, that operation is simple and easy to perform in a single frame.

Generally, I want full control over what order my code is executing in. With a broken coroutine.yield, my only option for performing operations like that is to just toss the thread onto the next frame, where it gets executed in whatever order roblox decides on. The less control I have, the more problems appear due to unknown behavior within the engine, and the more time I have to spend diagnosing and working around these problems. In this case, there isn’t a workaround at all.

3 Likes

Oh, I wasn’t aware that RbxUtility was deprecated. It would be intresting if ROBLOX allowed for custom Signal creation, as Signals are pretty useful in general.

1 Like

Yeah, although that might be (hopefully) a sign of better things coming in the future. We’ll have to wait on that one.

I’m normally against ROBLOX modifying vanilla Lua functions and the same is true for this feature request. I think that fixing the behaviour described by @woot3 is a far better fix than introducing a new function.

Something I just realized:
wait(2^127) would act exactly like this (you can resume the thread as though that paused it - and to get the current thread you would just use coroutine.running())

Example usage to create an event:

local args
local create = coroutine.create
local resume = coroutine.resume
local unpack = unpack
local huge = 2^127
local thread

local function Fire(...)
	args = {...}
	resume(thread)
end

local function Wait(timeout)
	thread = coroutine.running()
	wait(timeout or huge)
	
	if args then
		return unpack(args)
	end
end

spawn(function()
	wait(1)
	-- fire thread after 5 seconds
	Fire("hi","after 5 seconds")
end)

wait(0.1)
print("hi","before wait")
print(Wait())

Issue is when the wait() finishes running, it will throw an error if the thread has been resumed (signal fired)
I don’t think it matters, but does anyone know a way to avoid it?

You’re going to run into some serious bugs doing this.

local t = coroutine.create(function ()
  wait(2)
  print(coroutine.yield())
end)

coroutine.resume(t)
coroutine.resume(t)

wait(2) will be resumed by the second call to coroutine.resume and then the call to coroutine.yield will be resumed ~2 seconds later by the Roblox scheduler. I advise against this completely if you can’t ensure the wait time is longer than the expected lifetime of the thread.

I don’t think what you’re saying is true because wait() only works in roblox threads - where coroutine.yield() doesn’t (and acts as wait()) - and it will also convert all threads it is in to roblox threads (so your coroutine.create() will be converted to a roblox thread - and hence coroutine.yield() will act as a wait())

But yea wait can definitely be unreliable if you’re working in coroutines and will need to yield later. However, for my use case it satisfies it (trying to make my own RBXScriptSignal).

Edit:

local args
local create = coroutine.create
local resume = coroutine.resume
local unpack = unpack
local huge = 2^127
local thread

local function Fire(...)
	args = {...}
	resume(thread)
end

local function Wait(timeout)
	thread = coroutine.running()
	wait(timeout or huge)
	
	if args then
		return unpack(args)
	end
end

spawn(function()
	wait(2)
	-- fire thread after 5 seconds
	Fire("hi","after 5 seconds")
	wait(3)
	print("yayyayay")
	Fire("hi","after 5 seconds")
end)

wait(0.1)
print("hi","before wait")
print(Wait(3))
print(Wait())

Just discovered this (not sure if it was what you were saying and I just misinterpreted), but it turns out that past waits will mess up future ones.

This thread discusses possible motivations behind the change, and potential issues with assuming how the scheduler is implemented:

I would appreciate it if you tested my code before assuming what it does in future.

Calling wait inside of a coroutine will add it to the list of yielded threads in the roblox scheduler, after the time has elapsed the thread will be resumed. Upon its next yield the scheduler will review the reason for the yield and make a decision. Specifically in the case of coroutine.yield the scheduler will add it to the list of waiting threads with the duration for resume being the minimum wait time.

The code I have given you will indeed be added to the roblox scheduler. The scheduler however won’t see the second call to coroutine.yield since this happens before the 2 seconds elapse, all it knows is that the thread is yielded and needs to resume after 2 seconds.

I am suggesting that the error you are seeing, you won’t always see. In some cases it’ll actually resume another yield.

It’s not ideal to shove undead threads into thread scheduler forever though; they won’t get garbage collected and may result in memleaks (esp since they may contain a large stack).

1 Like

I did try this code, I got confused by the fact that you were using wait() in a coroutine and then yield after - something that doesn’t work (it produces the same results as just having one resume):

local t = coroutine.create(function ()
  wait(2)
  print(coroutine.yield())
end)

coroutine.resume(t)
--coroutine.resume(t)

But I think I understand what you’re saying now.

Also @Shining_Diamando, this is a stack of size 100 with 10,000 different yielded wait threads concurrently and I don’t lag at all:

local resume = coroutine.resume
local create = coroutine.create
local huge = 2^127
local wait = wait

local function makeStack(x)
	x = x or 1
	if x > 100 then
		local thread = create(function()
			wait(huge)
		end)
		resume(thread)
		resume(thread)
	else
		makeStack(x+1)
	end
end

for _=1,10000 do
	makeStack()
end

You can try changing the numbers and it will still work (there’s only an initial delay of actually creating everything in 1 frame / however ROBLOX does it (idk))

Sorry for getting a bit too technical, but, there’s a bit of a mistake in that code.
image

The stacks you are creating are actually of small sizes (largest being the overall function, of 10).
The fact is, what is likely happening is the following:

  • You are forcing the creation of a couple thousand threads with a small stack size
  • The threads are being resumed twice, which means it’ll escape the wait and leave a dead reference, likely with no stack inside thread scheduler
  • Due to the threads escaping the wait, their components will likely get garbage collected due to not being needed anymore
  • The stack from the recursion that lead to the thread being created has also already been cleared due to it going out of scope (calling itself and returning almost right after)
  • And finally, the only real things sticking around are probably the upvalue references to wait and huge from within the thread, but this is likely either a pointer (taking little to no mem) or cleared altogether.

I’m not 100% sure how the ROBLOX thread scheduler works or the garbage collectors handles the threads but this seems like what would be happening, due to the coroutine.yield bug and whatnot.

Consider this modification of woot3’s code

local t = coroutine.create(function ()
	local start = tick()
	wait(2)
	print("Test 1: " .. tick() - start)
	print(coroutine.yield())
	print("Test 2: " .. tick() - start)
end)

coroutine.resume(t)
coroutine.resume(t)

the results are
Test 1: 1.4305114746094e-06 – effectivly zero
Test 2: 2.027410030365

The resumption of the wait(2) by the second call to resume causes the yield to suspend the thread ~2 seconds rather than the assumed minimum wait time. This can be surprising if you’re not expecting it.

Now consider the results if I comment out that second resume

Test 1: 2.000331401825
Test 2: 2.0392727851868

The wait(2) waited 2 seconds and the yield acted like a true wait().

EDIT: and this is even more worrying

local t = coroutine.create(function ()
	local start = tick()
	wait(2)
	print("Test 1: " .. tick() - start)
	wait(4)
	print("Test 2: " .. tick() - start)
end)

coroutine.resume(t)
coroutine.resume(t)

results in

Test 1: 1.1920928955078e-06
Test 2: 2.0153305530548

So the value of wait(4) was ignored. This could cause real issues with a wait(2^127)

@woot3 Is there anything wrong with this code?:

local create = coroutine.create
local yield
local resume
local running
do
	local cresume = coroutine.resume
	local crunning = coroutine.running
	local wait = wait
	local type = type
	local huge = 2^127
	local threads = {}
	
	function yield(timeout)
		local thread = crunning()
		local val = threads[thread] or 0
		threads[thread] = val
		
		if timeout then
			cresume(create(function()
				wait(timeout)
				if thread then
					cresume(thread)
				end
			end))
		end
		
		wait(huge)
		threads[thread] = val+1
		thread = nil
	end
	
	function resume(obj)
		if type(obj) == "thread" then
			cresume(obj)
		else
			local thread = obj[1]
			if threads[thread] == obj[2] then
				cresume(thread)
			end
		end
	end
	
	function running()
		local thread = crunning()
		local val = threads[thread]
		val = val or 0
		return {thread, val}
	end
end

local thread = running()
resume(create(function()
	wait(2)
	resume(thread)
end))
print("started")
local start = tick()
yield(1)
print(tick()-start)
yield(3)
print(tick()-start)
print("finished")

This code prettty much attaches a resume to one specific yield by storing the version and incrementing it each time you yield. You can also choose to increment it in the local thread object (if resume is successful) and can make a method “getLatestThread” which takes an old version of the thread object and updates the version id.

@Shining_Diamando Sorry, heres a better example:

local resume = coroutine.resume
local create = coroutine.create
local huge = 2^127
local tostring = tostring
local wait = wait

local usedThreads = {}

local function makeStack(x)
	x = x or 1
	if x > 1000 then
		while true do
			wait(huge)
			print("A")
		end
	else
		makeStack(x+1)
	end
end

for i=1,10000 do
	local thread = create(makeStack)
	resume(thread)
	resume(create(function()
		while wait(1) do
			resume(thread)
		end
	end))
end
print("finished")

Idk why I couldn’t do while wait(huge) do , I had to resort to while true do wait(huge)
I guess you can argue that the stack size isn’t big for this one (only 1000 total variables), but realistically I doubt you’re ever gonna reach worse cases?