Coroutine.yieldwait(thread)

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?

This case would actually be more efficient as you’re not even holding any local variables or upvalues for later use so they’d likely be flushed out by the time everything is running.

Besides the looped thread reference, but that’s only 4 bytes for pointers times the 10000 times it is created. Not much in the big scheme.

What if after calling makeStack(x+1), there were a print(x)? Would that make it better? (Sorry I can’t test, don’t have computer access right now) Or could you make an example xd