Allow us to use : to call coroutine functions


#1

It is currently messy and a bit inconvenient to create and run a new thread in situations where you must pass arguments to the function and therefore can’t use spawn(). Example:

function myFunc(x, y, z)
    print(x, y, z)
end

coroutine.resume(coroutine.create(myFunc), true, 5, "hi") --this is ugly

My idea is to give all threads the metatable {__index = coroutine}, so that we can call functions in the coroutine table like this:

function myFunc(x, y, z)
    print(x, y, z)
end

coroutine.create(myFunc):resume(true, 5, "hi") --much nicer

Can we have this please?


#2

Isn’t this the whole point of coroutine.wrap? To provide an easy shorthand? Also, any reason you’re using coroutines instead of spawn when you’re not preserving the coroutine object?


#3

For the time being, you could do this:

-- Untested code, it might not work.
local makeCoroutine = function(callback)
    local c = coroutine.create(callback)
    local value = {}
    value.__index = c
    return value
end

#4

coroutine.wrap doesn’t actually function identically to coroutine.resume(coroutine.create(…))


#5

resume(create(f)) eats errors. wrap(f)() doesn’t eat errors.
wrap is generally preferable for that reason.


#6

The reason you can’t use spawn() there is because spawn() doesn’t allow you to pass arguments to the function serving as the thread.


#7

There are very few use cases for spawn.

If you just want to make a new thread, use wrap instead.
There’s usually no reason to yield on the legacy 30hz pipeline before resuming.


#8

Touching the built-in functions just seems a bit… naughty. You’re free to write your own wrapper if the syntax is ugly.


#9

You can call the function you want to make a thread of within the spawned thread to pass parameters.

spawn(function()
    myFunc(param)
end)

#10

The one caveat with that though is that spawn is not executed immediately, but rather after the next yield. So if you need it to execute immediately, coroutines are the way to go.


#11

I’ve been wondering about this. When is it actually necessary to execute immediately, that isn’t covered by just calling the function directly?

Consider this example:

CallA()
coroutine.wrap(function()
	print("foo")
	wait(1)
	print("bar")
end)()
CallB()

This can be rewritten to remove the coroutine entirely:

CallA()
print("foo")
delay(1, function()
	print("bar")
end)
CallB()

How about this more complex example:

CallA()
coroutine.wrap(function()
	for i = 1, 10 do
		print("foo", i)
		wait(1)
		print("bar", i)
	end
end)()
CallB()

This one is interesting, because it contains behavior that I would consider undesirable. That is, the first “foo” action executes before the caller, while every subsequent “foo” and “bar” action executes after. Surely it would make more sense for these actions to be grouped together in relation to the caller.

This can be done by either calling/inlining the function directly, causing the caller to depend on the actions, or using spawn, in which case the caller does not depend on the actions. By avoiding coroutines, the order of execution becomes more clear.

-- Everything occurs before CallB().
CallA()
(function()
	for i = 1, 10 do
		print("foo", i)
		wait(1)
		print("bar", i)
	end
end)()
CallB()

-- Everything occurs after CallB().
CallA()
spawn(function()
	for i = 1, 10 do
		print("foo", i)
		wait(1)
		print("bar", i)
	end
end)
CallB()

Signals also have this coroutine-like behavior. It makes me wonder how many bugs are caused by things being unintentionally executed out of order.


#12

There were only a handful of cases where coroutine.wrap has been useful for me, but it’s all been about latency;

coroutine.wrap(function()
    local result = ask_server_for_data() -- don't want to waste a cycle, slowing down retrieving data
    -- use result
end)

-- do ui stuff here (never want to delay this a frame)

If spawn would simply run the spawned thread in the same event cycle (which I’d prefer in pretty much all cases), but after the current thread suspended itself, that’d fully eliminate those use cases for me.


#13

I specifically use spawn so that the code does not run until the next yield. Waiting for roblox to move the character after firing CharacterAdded, for instance. I use coroutines when I want it to run immediately. I don’t think there is any other way to resume as quickly as spawn does.


#14

I’ve never had any issues with spawn not running immediately, it runs fast enough to where it’s not noticeable at all.


#15

Latency is not a concern, order is. code after spawn runs first, and then spawn runs at the next yield. coroutines run first.


#16

You shouldn’t be relying on side effects like that in multi-threaded code.


#17

True in real multithreaded environments, but Lua isn’t actually multithreading (hence the name “co-routine”). It’s more-so just cleverly running code on a scheduler. Thus, you can be sure that code right after a spawn will run first, unless the code after has some sort of yield (e.g. wait or http:GetAsync).

Can you imagine the nightmare that would ensue if Lua had actual multithreading? I guess we would all become experts at properly writing multithreaded code!


#18

I understand Lua isn’t real multithreading, but it’s close enough to where I feel relying on side-effects like that is hacky to me. It also, to my knowledge, relies on there being no yields anywhere else in your game while the code you’re expecting to finish up for spawn to run next concludes, which may not be realistic to expect.

Generally, I don’t use coroutines anywhere and just stick to spawn instead. The only times I’d use coroutines is if I actually needed to use the rest of the functions coroutine has to offer other than just using spawn and forgetting about it (i.e. if I wanted to pause the thread).


#19

I actually wish Lua had a well defined way to deal with the task scheduler.
Atleast in C, I can guarantee exactly what order something will occur in, using constructs like a mutex.
In Lua, I instead have to rely on check and set loops, kinda like a pseudo mutex.