Coroutine.resume in a ModuleScript can break require

I felt like mentioning that using coroutine.resume in a module can break anything that requires, couldn’t find it elsewhere.

If you’re wondering what I’m refering to, trying to require this module will cause your script to yield indefinitely.

local co = coroutine.running()

task.defer(function()
  coroutine.resume(co)
end)

coroutine.yield()
return "meow"

First of all, we should figure out how require works, when you require a ModuleScript object in Roblox, it creates a new thread to run that module in, or, in a theoretical Lua sense, it would be this

local function require(module)
  local f = debug.loadmodule(module)
  local thread = coroutine.create(f)
  return require_arg_capture(module, coroutine.resume(thread)) -- imagine this does the return caching or whatever, its not relevent here
end

If you understand how coroutines work, you might already see the issue here. But for those who don’t let me explain how yield/resume work.

When a thread is resumed, the thread that called coroutine.resume is marked as the thread that gets the response when you return or yield (completing as I like to call it), when require is called, its assumed this will be the script that required it.

However, because we resume in a task.defer block, require loses its response expectation and will never receive it.

If you add a print here…

task.defer(function()
  print(coroutine.resume(co))
end)

You’ll notice the MEOW goes here, and not the script calling require. The require will never get the meow because when a thread returns, it’s killed and will never resume again.

So why doesn’t this happen with task.spawn, simple, it doesn’t want a response. Because of this, the require is still treated as the thread that’ll get the response.

In summary, using coroutine.resume can cause require to break because of how coroutines handle their responses.

3 Likes

…if an engineer sees it, why are modules spawned in their own thread?

I assume for debug traceback reasons plus practicality.

Every script is essentially a separate green thread, and modules are just scripts that run only once. It would be awkward if the module ran on the same thread as the first caller but not the proceeding ones.

actually the problem is coroutine.running(), which for some reason works contextually, unlike coroutine.yield() which does not. Look at the following code

--ModuleScript
local co = coroutine.running()
return co

--Script
local co = require(script.Parent.ModuleScript)
local co1 = coroutine.running()
print(co == co1) -- false

However coroutine.yield() will pause the script thread.

It is strange that coroutine.running() does not return the Script thread. From a programmer’s point of view, a ModuleScript should not run on a separate thread because it is just a way to reuse code. In my opinion, coroutine.running() should behave like coroutine.yield().

In the case of your code, the Script does not resume because coroutine.resume(co) resumes that other thread, not the Script thread. So the script thread is not dead, but paused. If you get its reference you can resume it normally.

In these cases, what is typically done is to get the current thread reference (in its proper context) and send it to other contexts (in this case to the ModuleScript).

--ModuleScript
return function(co)
	task.defer(function()
		coroutine.resume(co)
	end)
	coroutine.yield()
	return "meow"
end

--Script
local co = coroutine.running()
local a = require(script.Parent.ModuleScript)(co)
print(a)
1 Like

require spawns a new thread which wraps the main proto of the module, which is why coroutine.running() of the script =/= coroutine.running() of the module.

coroutine.running just returns the active thread, its nothing special when it comes to thread control.

however that does not fit with coroutine.yield().