ModuleScripts that yield with coroutine.yield() will break parent thread

If this is a server script:

print'before'
require(script.ModuleScript)
print'after'

and it has this ModuleScript as its child:


local t=coroutine.running()
delay(1,function()
	coroutine.resume(t)
end)
print'yielding'
coroutine.yield()
print'resumed'
return nil

Then the output will be

  before
  yielding
  resumed

The ‘after’ is never printed, while it should be.

If anyone happens to know a workaround, please let me know.

1 Like

The require function doesn’t support yields like this, but if you want to yield immediately upon requiring a module, you could put the logic into a function and call it right after requiring, like so:

Server Script

    print'before'
    module = require(script.ModuleScript)
    module.init()
    print'after'

ModuleScript

    local module
    module.init = function()
        local t=coroutine.running()
        delay(1,function()
    	coroutine.resume(t)
        end)
        print'yielding'
        coroutine.yield()
        print'resumed'
        return nil
    end
    return module

Why does wait work?

print'yielding'
wait(1)
print'resumed'
return nil
Where did the coroutine come from Can I use coroutine.yield/resume on it Can I use wait/event:wait()/etc
I called coroutine.create myself yes no*
It's a "ROBLOX thread" (from delay/spawn/event:connect()/a script's main thread/etc) NO yes

*Maybe, but treat the coroutine as being a “ROBLOX thread” from then on

The core problem is that ROBLOX internally uses (the C side equivalent of) coroutine.create/yield/resume to implement its “threads”. Therefore when you use coroutine functions on ROBLOX threads, you interfere with what the engine does to its coroutines. And this works sometimes, but I wouldn’t rely on it. You might break assumptions the engine makes about the states its threads are in (like in this case), and even if you get it working, engine changes could make it break on you.

wait works because it’s coded to play nice with ROBLOX threads, including in a ModuleScript. But you manually yielding the ModuleScript’s thread probably causes the engine to lose track of that thread, meaning scripts waiting on the ModuleScript’s initialisation aren’t woken.

If you must yield in this context, you need to use a ROBLOX yielding function (like event:wait() on a BindableEvent).

2 Likes

Are you sure this is true? I use coroutine.yield in event connections after [Live] Changes to coroutine.yield.

CC @woot3 (I cc them in all coroutine/Roblox thread scheduler related posts)

My knowledge might be dated.

I think that change might actually be the problem here, kind of. Seems like calling coroutine.yield() now causes the task scheduler to relinquish control of the thread, and that leaves the ModuleScript in limbo.

(obviously, it still wouldn’t have done what OP wanted before the change).

So maybe this can be fixed. But things like this are why I recommend avoiding it.

local sig=Instance.new'BindableEvent'
delay(1,function()
	sig:Fire()
end)
print'yielding'
sig.Event:Wait()
print'resumed'
return nil

So strangely this works… I thought that after this change [Live] Changes to coroutine.yield there became no difference between Roblox threads and coroutine threads

On the 10th of September I enabled a change which allowed you to call coroutine.yield in any sort of context, essentially making a user-owned thread and Roblox thread synonymous. coroutine.yield is not at fault.

The problem is related to continuations, an internal mechanism used to pass information between a thread and the Roblox scheduler. In this specific case, the continuation is used to resume all threads requiring the module. coroutine.resume does not support continuations as it exists outside of the Roblox scheduler. Therefore, the script which called require will never resume.

You can see this in action with the following example (note how coroutine.yield is not called).

local t=coroutine.running()
delay(1,function()
	coroutine.resume(t)
end)
print'yielding'
wait(1e5)
print'resumed'
return nil

If the thread is resumed by the Roblox scheduler, continuations will run.

local t=coroutine.running()
delay(1,function()
	coroutine.resume(t)
end)
print'yielding'
coroutine.yield()
print'resumed'
wait()
print'resumed2'
return nil

I have every intention of fixing this at some point in the future, however it’s not as trivial as it may seem. By rerouting coroutine.resume to use the same resume mechanism as other threads, there is a significant performance impact which I would like to avoid.

5 Likes

Will you implement a temporary fix(that makes coroutine.resume slower)? Or how long will it be until the fix is live? Thanks

At present I am not working at Roblox due to my studies therefore I am unable to give you a timescale in which this will be fixed. Another member of staff could pick this up before I return. I can definitely ping somebody about this, however I would not expect a fix in the near future.

A short-term fix which makes coroutine.resume slower is unlikely to happen due to the significant impact it would have on performance.

For now, you might be better off just using a BindableEvent instead.

2 Likes

Are bindableevents slower than coroutines?

Not with regards to any metric worth caring about (instantiating a bindable event, connecting it, and firing it takes microseconds more than a coroutine).

Then why is using their implementation for coroutines such a worry : /

Or why dont they use coroutine’s buggy one :((

I believe you mean the Roblox implementation of resume. @Kampfkarren is saying that performance is unlikely to be a concern for you, however that does not mean the Roblox engine should not be performant.

Then require would fail 100% of the time.


The slowdown you get from using a BindableEvent is comparable to that of the temporary fix for coroutine.resume. It seems reasonable, that until a performant fix is shipped, to use a BindableEvent as an alternative.

2 Likes

I will be using bindableevents because thats the only way for me to continue scripting in my framework(and working on Roblox at all for that matter)
But this means everywhere i use coroutine resume i have to use bindableevents :confused:

I very much appreciate the Roblox engine being performant but I just don’t understand why buggy performant code was chosen over stable code…and why nothing will be done about this either (until months when you can return to fix it :/)

At some point I am going to put together an alternative to the coroutine library which handles all of this for you.


Before the change to coroutine.yield this bug was less prevalent as there was no real reason to resume a thread with continuations. We definitely appreciate that stable code is a priority, however if we’re going to change it then we should just do it right the first time.

1 Like

Thanks, I just have a couple of questions about it. Will it be a part of the Roblox engine or an auxiliary lua module? Also is this the only coroutine related bug it would solve or are there others I should be aware of? I’ve been having incomplete stack traces in my games too, would this be fixed? (I don’t have a repro atm but I suspect it’s to do with coroutine resume because I never did replicate it when trying to with coroutine wrap)

I guess the issue isn’t important enough/doesn’t affect enough people to necessitate a temporary fix

The library will be a Lua module, as a temporary alternative to the coroutine library.

Unfortunately I do not have a copy of the source code to hand to answer those questions. I believe there are some other related bugs this may fix, I am not certain if stack traces is one of them.

1 Like

It’d be nice if this could be revisited

the problem for me is I can’t think how to avoid memory leaks when using BindableEvents to replace coroutines because even

local x={}
coroutine.wrap(function()
	local t=setmetatable({x},{__mode='v'})
	repeat wait()until not t[1]
	print'gced'
end)()

leaks in lua

Sorry to revive this topic, but the new task library solves this problem. You should use task.spawn ( Takes a thread or function and resumes it immediately through the engine’s scheduler.) instead of coroutine.resume and it will work.

New version of your code (only changed the ModuleScript):

local t=coroutine.running()
delay(1,function()
task.spawn(t)
end)

print'yielding'
coroutine.yield()
print'resumed'

return nil

output:

  before
  yielding
  resumed
  after
7 Likes