Coroutine related question

Hello guys. I’m trying to make 1 game, and I want to use coroutines for it, to run multiple calculations at the same time. But, I got 1 question about “race conditions”, like this ones:

local mas1 = {}
for i = 1, 10, 1 do
    coroutine.wrap(function()
        task.wait(0.2)
        table.insert(mas1, i)
    end)()
end
task.wait(0.2)
for a = 1, #mas1, 1 do
    print(mas1[a])
end

Is it be possible, that second loop for will execute earlier than all coroutines finish? And if yes, how I can wait for every coroutine to execute before doing something?

I take it that’s just an example function because of how illogical it is (should use task.spawn over coroutine.wrap there), but anyway yes you could have a situation where the coroutine takes longer than your wait
You could make a table of your coroutines and check that they’re all done before continuing

local mas1 = {}
local coroutines = {}
for i = 1, 10, 1 do
    local cr = coroutine.create(function()
        task.wait(0.2)
        table.insert(mas1, i)
    end)
    coroutine.resume(cr)
    table.insert(coroutines, cr)
end
task.wait(0.2) -- Minimum time they'll take anyway
for i,v in pairs(coroutines) do
    if coroutine.status(v) ~= "dead" then
        repeat task.wait() until coroutine.status(v) == "dead"
    end
end
for a = 1, #mas1, 1 do
    print(mas1[a])
end

Perfect place to use Promises.

Code
local RandomGenerator = Random.new()

local numbers = {}

local promises = {}

for i=1, 10 do
	local promise = Promise.new(function(resolve, reject)
		task.wait(RandomGenerator:NextInteger(1,10)/10)
		table.insert(numbers, i)
		resolve()
	end)
	table.insert(promises, promise)
end

Promise.all(promises):andThen(function()
	print(numbers)
end):catch(function(errorMessage)
	print("One or more promises rejected.")
end)

Promise.all in evaera’s implementation takes multiple promises (running in safe, independent threads) and packs them into one promise that is

  1. resolved once the last living promise is resolved.
  2. rejected if any promise is rejected.

As the printed table shows, order of completion is not important. Function bound to andThen will execute when the last running operation resolves.

Update:

If you are not keen on using Promises, refer to other replies.

1 Like

in this case, yes, it is possible that the second cycle will run sooner. some threads may take longer than the fixed time of 0.2 (the second task.wait).
To ensure that the second cycle runs later, just pause the current thread and count how many threads have run. If all 10 were executed, resume the thread. This is the simplest and most correct way to handle threads.

local mas1 = {}
local count = 0

local thread = coroutine.running()
for i = 1, 10 do
	coroutine.wrap(function()
		task.wait( some random time )
		table.insert(mas1, i)
		count += 1 
		if count == 10 then
			coroutine.resume(thread)
		end
	end)()
end
coroutine.yield()

for a = 1, #mas1 do
	print(mas1[a])
end

Actually there are many reasons why this is a bad idea. But the main one is that promises are not made to solve this kind of problems. Remember the “Callback hell” in asynchronous programming, many things can go wrong in very complicated ways, that’s where a promise should be applied.

I appreciate the critique. If you don’t mind, I’ll extend my previous post with this one for further analysis. I’m also inviting constructive criticism in case something could be done and thought of differently.

Sorry for a tad lengthy post.


On using coroutines or task

Your proposal of handling threads is indeed the simplest solution so far, and I have hence removed my slightly longer version from the previous post in favour of yours.

However, I wouldn’t say the most correct approach exists. Every approach has its pros and cons, just like various programming paradigms, design patterns or anything else.

  • Counting threads

Counting threads is perfectly fine as long as the called functions are pure and/or we are confident enough that nothing will fail.

If any of those coroutines in the code fail, the main thread will never be resumed. The risk can be mitigated by going another path, or running all these coroutines in a separate thread or as a promise in order to have a chance to respond to errors.

Counting threads is as reliable as callbacks from each of the coroutines.

  • Polling

Generally events are preferred over polling. But since coroutines don’t send any signals when their status changes, we can make an exception and run active checks for living coroutines. I’ve seen this successfully used before, and it can respond well to failures.

An example of this is @Bedu009’s code above (except task.wait(0.2) should be removed).


On promises

Could you please elaborate, why are promises not be suitable?

Promise implementations in Luau lean on coroutines, and have certain error handling benefits.

More

Promises can be pipelined, provide success and failure handlers upon completion, or be cancelled. Coroutines are also known for occasionally confusing stack traces.

You mentioned “callback hell” (famous term from JavaScript), where promises are first class citizens, mainly used for safe async work as an alternative to continuation-passing style (that can result in callback hell).

The concept of promises alone had existed almost two decades before JavaScript was introduced. They can be used in file I/O, networking, evaera even used them with tweens and animations (if that’s your style).

They are designed to represent a value that may exist now or some time in the future.

Real multithreading

(Coroutines actually thread cooperatively, meaning they are resumed one at a time in the background.) Intensive operations can optionally run in parallel: Parallel Luau | Documentation - Roblox Creator Hub.

Very large calcuations, intense path finding, terrain generation, any form of rapid hitcasting fall in this category.

In reality, there is a most correct way; that is to say, if there were not, there would not be so many languages, paradigms, patterns, etc. It would be enough with just a couple of them to do everything. With a pair of pliers I can do everything, even drive small nails, but the hammer is the most correct tool.

I can’t think of a reason why they should be pure; perhaps you could give me an example where it wouldn’t work if there is no purity.

The possible failures you describe are failures of logic, not of the technique (counting coroutines). So, to apply the technique, you must contemplate all possible failures of logic. The most general is to think that they can all fail and never resume the current thread (in the case of my example). In this case, I would solve it by creating a parallel timer. If the timer terminates and not all threads have terminated, resume the current thread with a failure flag or something like that. But a more appropriate solution will depend on each case. See how these are logic failures?

Obviously, I didn’t invent this; I actually learned it from a Roblox developer in a post he made on this forum, but I also saw it in some articles on asynchronous programming. It’s even in the promise code (check the Promise._all method).

Certainly, counting is one of the things computers are best at; what can go wrong there?

First we must differentiate two very different things: promises as a pattern in asynchronous programming and Evaera’s implementation of promises.

In roblox coroutines were chosen over promises for a good reason, coroutines are more general than promises. So if you are already using coroutines to do asynchronous programming, why would you use promises? It sounds like, having many types of hammers, one prefers to use pliers to drive nails. I hope the analogy was understood. Of course, some things with promises are easier, for example when you use HTTPService. There a lot of things can go wrong and you would like to catch all that in one place. With promises it’s easier. But usually you want a finer control, so coroutines are more suitable in most cases.

As for the implementation, a great effort was made to recreate javascript promises without taking into account the real needs in roblox development. For example, in javascript (web development) the communication between client and server is manual and the developers have control over the replication where many things can go wrong and the promises are ideal in that case, in roblox the replication of Instances is automatic, basically the only thing we have to worry about is if they will arrive or not, or when they will arrive (kill a fly with a cannon). The huge code base (according to lua standards) of almost 2000 lines for something that should be one tenth of that. No doubt there will be a lot of dead code and several unnecessary layers that should be crossed anyway. there are a few more, like stack tracing or dynamic checking, but it’s getting too long.

A lousy example, but why not learn the hard way? I propose you to create a game where all the animations are handled with promises, then you will have it clear. No doubt Evaera is a talented programmer, but when it comes to promises…

1 Like

You made some very fair points and they make a lot of sense!

I don’t have a practical example at hand. In most cases, I’d sleep well relying on your code. That quote of me is only a partial excerpt: “… as long as the called functions are pure and/or we are confident enough that nothing will fail.”

Discovering all logical errors and preventing all runtime errors in growing code can be rather tedious, often requiring extensive testing. So the more “pure” functions are, the less likely runtime errors should occur.

As far as I know, in production code the goal is to have systems in place to recognize and respond to failure cases, preferrably without delays, preventing them from affecting the whole system, evidently depending on the task:



Absolutely not lol. I wanted to illustrate how promises are not necessary limited to web communication. For instance, I use them for core game loops, when fetching data, or where they seem practical to avoid race conditions, mostly when assembling something. Not overusing.

It also has to do with vanilla Lua’s historical design philosophy that prioritized creating a lightweight and efficient language, so it natively doesn’t have concurrency abstractions. Developers have enough tools to build libraries and concurrency models themselves if they choose to. After all, Lua doesn’t even have a wait or sleep function by itself.

Promises (Quenty also implemented his version) are just one of the means to control async operations, which can sometimes prove useful over robust coroutine work.



By the way, I found this cool discussion, maybe we might get some more control with task that currently promises provide.


Finally, about the most correct way (in hidden details to stay on topic):

At first, this stroke me as a bold statement, considering not only programming in general, but everything in life. Nevertheless, it can in fact reasonate with a person.

Funny, after taking a moment to ponder, I say it’s both ways. There is a most correct way, but there is also not.

Different programming languages seem to often come with trade-offs in relation to one another. All of them work with similar prepositions, so to say, but some focus on specific domains, others are more general, some are thus faster, others slowers but sometimes arguably better.

A good programmer (consciously or not) follows at least some programming principles that exist in attempts to steer themselves towards writing better code.

On the flip side, suppose we’re making an advanced NPC. We could decide to make a finite state machine, which is more simple than a behavior tree, but can become hardly manageable with loads of states. What if we instead went for goal oriented action planning? Utility system? Unstructured performant code with if statements (not always the best path but hey)?

No one is always right, but when they are not, they are not necessarily wrong. :slight_smile:

And why not? or how else will you learn?. something small with lots of animations would be fine. i did it and was fun.

I see your point: there is no way to know which is the “correct” way. But among the many that exist, we can choose the “most correct” one, that is, the one that is closest to the correct way. In your example, we have state machines, behavioral trees or any other form of AI, each with its own characteristics and possibly none would fit exactly what we want. So, we could say that none of them is the correct one. But we don’t stop there. Actually, what we do is we look for the most correct way possible. We do two things: first, we look for information in the huge knowledge base of game development (and other related branches) looking for solutions to problems similar to ours and what their results were. Second, we adapt our needs, that is, there are things we have to give up because they are not possible with the current tools; we set achievable goals. By being informed about how others have solved similar problems and having set more achievable objectives, we can make a better decision about which tool is the most correct in our case. Nothing guarantees that our decision was the best one, but at least we came pretty close.

1 Like

i’d hate to bear bad news but the odds of this feature ever being implemented is near 0%, so writing your own task.yield/task.await pattern or importing the Promise library would be a better option

i made the feature request with the assumption that the compiler or whatever runs dev code in runtime has access to memory addresses, so i believed (in theory) it would be passed a reference to the variable in question and “try” the reference until it met the yield condition

--- a case that would solve mr cozidatel's case if task.yield/task.await existed
local mas1 = {}
task.await(#mas1 >= 10)

onto the problem at hand:

Here is a task.await() function, that for your case, will yield the thread until #mas1 >= 10
It will yield whatever thread its called in until the function passed in it returns a truthy value

local function t_await(Fun: () -> boolean)
	local Result = Fun()
	if not Result then
		repeat Result = Fun(); task.wait() until Result
	end
end
local mas1 = {}

for i = 1, 10 do
	task.spawn(function()
		task.wait(0.2)
		table.insert(mas1, i)
	end)
end

t_await(function() --yield the thread until #mas1 >= 10
	return #mas1 >= 10
end)

--will print after approx 0.2s because of the task.wait(0.2) call
--{...}, 10
print(mas1, #mas1)