Wait breaks coroutine.yield

coroutine.resume(coroutine.create(function()
    wait()
    while true do
        coroutine.yield()
        print("resumed?")
    end
end))

wait resumes the thread and if you try to yield it again, it resumes that too.

This is a quirk of how Roblox does scheduling.

If Roblox were to fix this, they would have a few choices:

  • Make coroutine.resume yield until the coroutine calls coroutine.yield. This would break existing code that expects to run in “parellel” with the new coroutine.
  • Don’t make coroutine.resume yield: instead, when coroutine.yield is called on a Roblox-scheduler-managed coroutine, the coroutine stops being managed by the scheduler. Keep in mind that its results were also not returned to resume! You would need a loop running using coroutine.status in order to find out when your coroutine calls coroutine.yield. This would break existing code that expects roblox scheduling to work in new coroutines.
  • Make wait and other internal methods that rely on the scheduler do nothing or error if used in a non-scheduler-managed coroutine. This sounds like it would be very messy internally, and it would break existing code that expects roblox scheduling to work in new coroutines.

I don’t think that changing the behavior of the existing coroutine functions is a good idea. I think the best fix for this is a new library that does coroutine-like behavior, but is roblox-scheduler-compatible, ideally part of the Roblox engine.


It’s not part of the Roblox engine, but this thread prompted me to publicise my Cord module, which is essentially coroutines, but Roblox-scheduler-compatible. It might be useful to you.

The following is your example using Cords. It has the behavior I assume you expected coroutines to have. :resume() yields until :yield() is called, then it is not resumed again. It also passes parameters/return values around like coroutines do, but there’s none of that happening in this example.

Cord:new(function()
    wait()
    while true do
        Cord:yield()
        print("resumed?")
    end
end):resume()

This would be nice, although breaking existing code isn’t very good.

A fourth option would be adding something like coroutine.unschedule which would remove a coroutine from the scheduler. Current behaviour remains the same, but now we can use that new function to finetune stuff to our own likings.

1 Like

I was actually looking into this a few weeks ago and believe I have a fix for it. The problem is less to do with wait and actually how the scheduler views coroutine.yield.

Calling yield implies you wish to pause the thread and resume it at a later time with coroutine.resume. If you call yield from within a Roblox thread it will be treated the same as calling wait with no arguments. By calling wait in a coroutine you add that thread to the scheduler and as such it is treated as a Roblox thread.

The solution is to ensure that yield is never resumed unless explicitly calling the resume method.

Some games rely on the behavior that yield acts as wait, by correcting this behavior they would break.

As a temporary solution you can use a single bindable event and use Event:Wait() to yield the thread without it being resumed until you call Fire. In turn you can wrap this method and use it in conjunction with the coroutine library to create a version with the expected behavior.

I noticed that having wait() in the thread caused coroutine.yield() to behave exactly like if you had called wait() instead, which you can see from wait() print(coroutine.yield()) because it returns the same values. How are games relying on this behavior?

Games do not rely on the behavior there, instead they rely on the behavior of yield in the ‘main’ thread.

Why would anyone expect a thread yielded with coroutine.yield to be resumed without coroutine.resume being called?

I am not sure, but I can tell you people definitely used to do this. The question is whether people still do.

The only time I’ve seen coroutine yielding/resuming and waits mix was when I tried making a cancellable wait

local thread = coroutine.running()
local resumed = false
spawn(function()
	-- something happens
	if not resumed then
		coroutine.resume(thread)
	end
end)
wait(5)
resumed = true

but this is a coroutine resuming a wait, not a wait resuming a yield. I’m not sure how else somebody would be mixing waits with yields/resumes or when somebody would write coroutine.yield() instead of wait() if they for whatever reason expect them to behave exactly the same. Do you know how people used to rely on this?

Looks like coroutine.yield is already documented to work the way that you’re proposing.
Was there a reason people used it instead of wait?

That’s kind of a recent thing. In the past, coroutine.yield() would yield the current thread and, if wait() was used before, leave it at the end of the thread queue. coroutine.yield() didn’t have a minimum wait time (like wait() has) so the thread would be resumed the same tick. If you had code like this:

local stop,count = tick()+1,0
while tick() < stop do
	count = count + 1
	coroutine.yield()
end
print(count)

You’d see that it would run thousands of time a second. Only reason it didn’t infinite loop is because the task scheduler automatically stops resuming threads after some time.
@zeuxcg (the man, the legend) once posted about this (here) with the message This will be fixed in a couple of weeks

Not sure how they fixed this. They probably made it yield for a minimum time (instead of 0/unspecified), but I don’t know how the task scheduler precisely works internally.

that reminds me: the wiki article is probably outdated on task scheduler + coroutine.yield() interaction

EDIT: and they also fixed the “exploit” to get the global environment ¯\_(ツ)_/¯

1 Like