Can Anyone Explain How You Are Supposed To Use Task Functions (task.wait() ) In A Coroutine?

Hello, thanks for reading my post.

  1. What do I want to achieve?

A better understanding of coroutines, as I am trying to learn how to effectively utilize them. Specifically, when practicing, I am often confused about how task.wait() interacts and/or prematurely yields a coroutine before its corresponding yield call.

  1. What is the issue?

Really, I am just looking for someone to explain to me the proper usage of task.wait() in coroutines, specifically in conjunction with while loops.

For example, I was just messing around in a Script and came up with this example:

local yieldNumber = 5
local baseNum = 0

local function solveMath()
	print("Coroutine is started")
	
	while task.wait(1) do
		print("Loop has been entered")
		baseNum = baseNum + 1
	end
	
	print("first yield about to happen")
	coroutine.yield(baseNum)
	
	task.wait(1)
	print("Loop finished")
	return "Done!"
	
end

local number		= coroutine.create(solveMath)

local success, result	= coroutine.resume(number)
print("Success: " , success)
print("Result: " , result)

task.wait(3)

local success, result	= coroutine.resume(number)
print("Success: " , success)
print("Result: " , result)

It seemed rather straightforwards to me. Upon calling coroutine create, the function would be called and wait 5 seconds (due to the loop) before the yield call, and the number 5 would be returned.

However, this is very far from the case. When running the above code the output looks like this:

Coroutine is started
Success: true
Result: nil
Loop has been entered (x2)
first yield about to happen
Success: true
result: 2
Loop finished

This is not what I would expect to happen, and in addition to that, I sometimes get a warning that states “task.wait should not be called on a thread that is already ‘waiting’ in the task library”.

I would be lying if I said I understood what this means.

In addition to that, removing the task.wait() and structuring the function like this:

local function solveMath()
	print("Coroutine is started")
	
	while baseNum <= yieldNumber do
		print("Loop has been entered")
		baseNum = baseNum + 1
	end
	
	print("first yield about to happen")
	coroutine.yield(baseNum)
	
	print("Loop finished")
	return "Done!"
	
end

works exactly as I expected the original attempt to. But I do not understand why putting a task.wait() inside of a while loop, which I thought was common practice, could mess up my interpretation of how coroutines worked.

Lastly, after watching a tutorial on the subject, I saw this chunk:

local wrappedCoActive	= false
local runTimes			= 0

local newWrapCo			=  coroutine.wrap(function(message)
	runTimes = 0
	
	while wrappedCoActive == true do
		runTimes+=1
		print(message)
		task.wait(1)
		
		if runTimes >= 3 then
			wrappedCoActive = false
		end
	end
	
	coroutine.yield()
	
end)

wrappedCoActive = true
newWrapCo("My message!")

And everything works exactly as intended. There are no issues using task.wait().

Really I would just appreciate some input about using task.wait() in conjunction with while loops inside of a coroutine, because I am struggling to understand how it all works together.

Thanks in advance

3 Likes

Threads/coroutines is a very confusing topic and requires a lot of researching and testing to confirm it’s behavior.

  1. coroutine.create does not immediately call the function. It returns a new thread/coroutine and has to be resumed with either task.spawn, task.defer, or coroutine.resume.
--!strict

local function foo(): ()
	print("called foo")
	coroutine.yield("bar")
	coroutine.yield("baz")
	return "qux"
end

local c: thread = coroutine.create(foo)

print(coroutine.resume(c))
-- called foo
-- true bar

print(coroutine.resume(c))
-- true baz

print(coroutine.resume(c))
-- true qux

  1. The use of threads/coroutines are supposed to be asynchronous. You weren’t meant to resume the same thread multiple times if the thread itself can halt (aka task.wait, excluding the use of coroutine.yield), and I believe it’s not until the release of the task library that the warnings were shown. The documentation page itself says this:

…(coroutine.resume) returns when the function either halts or calls coroutine.yield() and, when this happens, coroutine.resume() returns either the values returned by the function, the values sent to coroutine.yield(), or an error message. If it does error, the second return value is the thrown error.

What do I mean by resuming the same thread multiple times?

A thread created by coroutine.create will be the same thread no matter what. Calling coroutine.resume on this thread while the thread itself is still running is not the intended behavior and will actually “skip” the line where it halted at. Here is an example of what this process looks like:

local function foo(): ()
	print("called foo")
	workspace.Baseplate.Changed:Wait()
	print("Skipped baseplate changed listener")
	task.wait(9999)
	print("Skipped task.wait")
	coroutine.yield("bar")
	coroutine.yield("baz")
	return "qux"
end

local c: thread = coroutine.create(foo)

print(coroutine.resume(c))
-- Output: called foo
-- halted at workspace.Baseplate.Changed:Wait()
-- Output: true (void)

print(coroutine.resume(c))
-- skips workspace.Baseplate.Changed:Wait()
-- Output: Skipped baseplate changed listener
-- halted at task.wait(9999)
-- Output: true (void)

print(coroutine.resume(c))
-- skips task.wait(9999)
-- Output: Skipped task.wait
-- coroutine.yield("bar") gets called
-- Output: true bar

print(coroutine.resume(c))
-- coroutine.yield("baz") gets called
-- Output: true baz

print(coroutine.resume(c))
-- thread halts with a returned value of "qux"
-- Output: true qux

The same applies for coroutine.wrap, which essentially just wraps your thread in a function you can call and works similarly to coroutine.resume(coroutine.create(foo)). Your example also outputs the same warning if you call newWrapCo() again.


  1. The internal warnings simply shows unintended behavior. From the example I gave above, swapping workspace.Baseplate.Changed:Wait() to another task.wait() will give the warning output you are experiencing: task.wait should not be called on a thread that is already 'waiting' in the task library.

In fact, there are multiple warnings:

task.wait should not be called on a thread that is already ‘waiting’ in the task library

--!strict

local function foo(): ()
	task.wait(9999)
	task.wait(9999) -- task.wait should not be called on a thread that is already 'waiting' in the task library (from the task.wait on line 4) 
end

local c: thread = coroutine.create(foo)

coroutine.resume(c)
coroutine.resume(c)

task.spawn should not be called on a thread that is already ‘waiting’ in the task library

--!strict

local function foo(): ()
	task.wait(9999)
	task.wait(9999) -- task.wait should not be called on a thread that is already 'waiting' in the task library (from the task.wait on line 4) 
end

local c: thread = coroutine.create(foo)

task.spawn(c)
task.spawn(c) -- task.spawn should not be called on a thread that is already 'waiting' in the task library (from the task.wait on line 4)

task.defer should not be called on a thread that is already ‘deferred’ in the task library

--!strict

local function foo(): ()
	task.wait(9999)
	task.wait(9999) -- will surprisingly not show the task.wait warning when deferred calling
end

local c: thread = coroutine.create(foo)

task.defer(c)
task.defer(c) -- task.defer should not be called on a thread that is already 'deferred' in the task library

This is great, however it’s a lot at one time. So @ImTheBestMayne , here’s a summary of Tasks, and Coroutines.

Tasks and Coroutines are Thread Handlers, they allow for multi-threading (aka Parallel Scripting).

They allow you to control how threads, and processes are ran, and at what time.

Normally code executes line by line, a function called after another one will be yielding for the previously called function.

With Tasks and Coroutines you can create new Threads (which are really just processes), which run at different timings, other things don’t yield for them essentially.

Threads created using task, can be created by doing:

task.spawn()
Which will immediately create the thread, and the code will start running

To create a coroutine you do local myThread = coroutine.create(), which creates a Thread, however it doesn’t start right away. It must be resumed using:
coroutine.resume(thread), task.spawn(thread/function), or task.defer(thread/function)

These threads will be asynchronous, meaning they run at their own pace, but they won’t hold up anything. Ex:


task.spawn(function()
   task.wait(2)
   print("Thread Completed")
end)

local defaultFunction = (function ()
	print("Function Completed")
end)()

-- Output --
--[[
"Function Completed"

around 2 seconds later

"Thread Completed"
]]

And to answer your second question about task.wait and wait

Essentially, they do the same thing, however task.wait() is newer, and has better performance and time accuracy, as it’s updating faster.

Another difference, is that task.wait() refers to the current thread/process it’s in, if it’s just in a script it’ll just yield the script since the script itself is a separate thread/process.

Thank you for your response, but it is unfortunately not a tl;dr of my post.

Your post is explaining and utilizing task.spawn, but my post explains the behavior on resuming the same thread that has been halted and why coroutine.resume returns the way OP has described, and the unintended consequence of using task.wait in this scenario.

1 Like

Gotcha, I did miss that part, thanks for calling me out on that one. :yellow_heart:

Thanks for the long response.

I’ll have to reference it back when I have questions on the subject again. However, from what I gathered, you mean that task.wait() or any of the other wait methods you used inside of test functions basically calls coroutine.yield()? Just trying to better understand what you are saying, I made this simple example based off what you said:

local function funct()
	print("Hello")
	task.wait(10)
	print("Hello x2")
	return "What is up"
end

local co	= coroutine.create(funct)

local co2	= coroutine.wrap(funct)

coroutine.resume(co)
-- Hello

coroutine.resume(co)
-- Hello x2

-- VVV Ran on a separate test with the two coroutine.resume's commented out
co2()
-- Hello
-- Hello x2

I understand the info that you have laid out, but I would not say it has helped me better understand the functionality of a coroutine.

Another example:

local task1 = coroutine.wrap(function(part :Part)
	local changeColor = true
	local changeval = 0
	
	while changeColor do
		part.BrickColor = BrickColor.random()
		task.wait(.25)
		changeval+=1
		if changeval >= 15 then
			changeColor = false
			
			coroutine.yield()
			
			print("Hello")
		end
		
	end
	
end)

local task2 = coroutine.wrap(function(part :Part)
	local changeColor = true
	local changeval = 0

	while changeColor do
		part.BrickColor = BrickColor.random()
		task.wait(.25)
		changeval+=1
		if changeval >= 25 then
			changeColor = false

			coroutine.yield()

			print("Hello x2")
		end

	end

end)

task1(workspace.Part1)
task2(workspace.Part2)

task.wait(8)

task1()
-- Hello

task2()
-- Hello x2

Maybe I just need to practice with is more, but using task.wait() inside of both of these functions works the exact way that I would expect them to. Maybe it has to do with coroutine.wrap, or something else that I am missing, I still do not necessarily understand why one of the above two examples yields using task.wait(), but the other does not.

Maybe I just need to practice with the subject more.

More specifically, anything that halts. I say halt because returning also halts the execution. It’s not exactly the same as coroutine.yield since it returns void (which is not the intended usage in the first place if you plan on resuming the same thread over and over again) and differs from return since code resumption is not available.

As I said above - it is not the intended usage in this scenario. Threads/coroutines simply allow you to run multiple tasks at the same time.

When you create a thread using coroutine.create/coroutine.wrap, you are already creating a singular thread/function in memory. If you resume/call it the first time like your example, it works as normal. However, call it a second time shortly after while your previous call has not finished (due to yielding), you’re actually trying to “resume” your thread that’s already running. Since coroutine.resume’s unique property of being able to return something despite being asynchronous, your code isn’t allowed to halt or it will return void. If you were able to yield and return something later, then it’s synchronous, which is not the point of threads/coroutines.

What you want to do is create a new thread everytime you call your function, which is done by the conventional task.spawn on a function. task.spawn has 2 unique functionality: to create by passing a function or resume a thread by passing a thread. When we create a new thread, we aren’t resuming a thread that is already running.

In this code, none of these conflict with each other as they are all starting the code fresh from the top.

local function someTask(part: BasePart): ()
    while part.Transparency < 1 do
        part.Transparency += math.random(1, 10) / 100

        task.wait()
    end
end

task.spawn(someTask, partA) -- thread: 0x8c63b9eda1e719ea
task.spawn(someTask, partB) -- thread: 0x805780ad7f1f06ea
task.spawn(someTask, partC) -- thread: 0x8c50eb2a832717ea

You’ll also see some code using coroutine.create/coroutine.wrap used inside functions, but those wont conflict with each other since a new thread is being made everytime a function is called anyways. task.spawn(func) also achieves pretty much the same results.

-- damageOverTime. Immediately starts DoT and returns thread for cancellation
local function damageOverTime(humanoid: Humanoid, damage: number): ()
    local damageOverTimeThread: thread = coroutine.create(function()
        for i = 1, 10 do
            humanoid:TakeDamage(damage / 10)
            task.wait(0.5)
        end
    end)

    coroutine.resume(damageOverTimeThread)

    return damageOverTimeThread
end

-- starts DoT and waits 3 seconds
local damageOverTimeThread: thread = damageOverTime(me, 50)
task.wait(3)
-- cancel DoT
task.cancel(damageOverTimeThread)

coroutine.yield usage is very niche, and you’ll find that you’ll be just fine without them. You can use them to make custom iterators, counters, or my favorite, replacement for task.wait loops.

--!strict

local function characters(str: string): () -> (number, string)
	return coroutine.wrap(function(): (number?, string?)
		for n: number = 1, #str do
			coroutine.yield(n, string.sub(str, n, n))
		end
		
		return nil, nil
	end) :: any -- silence strict analysis
end

for position: number, character: string in characters("Hello, World!") do
	print(position, character)
end

-- which can be done without the use of coroutine.yield

local function characters(str: string): () -> (number, string)
	local n: number = 0

	local iterator = function(): (number?, string?)
		n += 1
		local character: string = string.sub(str, n, n)
		
		if character ~= "" then
			return n, character
		else
			return nil, nil
		end
	end :: any -- silence strict analysis

	return iterator
end

for position: number, character: string in characters("Hello, World!") do
	print(position, character)
end
--!strict

local generateUniqueId: () -> number = coroutine.wrap(function(): number
	local uniqueId: number = 0

	while true do
		uniqueId += 1

		coroutine.yield(uniqueId)
	end
end)

for _: number = 1, 10 do
	print(generateUniqueId())
end
--!strict

local message: string = "Do you want to browse my shop?"

local function shopDialogue(): ()
	-- show some dialogue to the user and show Yes/No buttons
	for n: number = 1, #message do
		someLabel.Text = string.sub(message, 1, n)
		
		task.wait()
	end
	
	yesButton.Visible = true
	noButton.Visible = true
	
	-- reference the current thread. we will be using it to
	-- yield and resume later on. we also store connections
	-- to disconnect them when the user is done.
	local mainThread: thread = coroutine.running()
	
	local yesConnection: RBXScriptConnection = yesButton.Activated:Once(function(): ()
		-- resumes the yielded main thread with the boolean true
		coroutine.resume(mainThread, true)
	end)
	
	local noConnection: RBXScriptConnection = noButton.Activated:Once(function(): ()
		-- resumes the yielded main thread with the boolean false
		coroutine.resume(mainThread, false)
	end)
	
	-- waits for the response and perform cleanup
	local response: boolean = coroutine.yield()
	yesConnection:Disconnect()
	noConnection:Disconnect()
	
	if response == true then
		someShop.Visible = true
	end
end
1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.