Coroutine.kill implementation

Say there is a thread that looks something like this:

local working
local function x()
	working = coroutine.wrap(function()
		while true do
			wait(0.1)
			print("working")
		end
	end)
	working()
end

wait(5)
coroutine.kill(working)
working = nil

The solution should work where multiple versions of x() are called in “parallel” and consecutively after the previous is finished

Ideally I want the solution to still allow yielding from inside the thread (as is done in this example through wait(0.1))

10 Likes

I guess something like this could work but it seems a little messy

local exists = {}

local function myWait(t)
	wait(t)
	if not exists[coroutine.running()] then
		coroutine.yield()
	end
end

local function myKill(thread)
	exists[thread] = nil
	if coroutine.status(thread) == "running" then
		coroutine.yield(thread)
	end
end

local function myWrap(f)
	local thread = coroutine.wrap(f)
	exists[thread] = true
	return thread
end

local working
local function x()
	working = myWrap(function()
		while true do
			myWait(0.1)
			print("working")
		end
	end)
	working()
end

x()
wait(5)
myKill(working)
working = nil

This assumes the coroutine yield change though too

2 Likes

You should implement a state variable, and have the inner loop check if the state is “dead”. Alternatively, you could use coroutine.yield instead of wait, and you could pass a state variable to your generator so that it gets returned by coroutine.yield. You should handle calls to wait() outside of the generator anyway, in my opinion.

This is an interesting point of discussion, because you can’t actually “kill” a coroutine unless it yields at some point, because in order for a method like “coroutine.kill” to be called you’d have to yield to the thread which handles calling that method, either with coroutine.yield or some other method that yields (e.g., wait()).

2 Likes

Do you mean to do something like this?

local threads = {}

local function x()
	local curr 
	curr = coroutine.wrap(function()
		while true do
			wait(0.1)
			if not threads[curr] then
				coroutine.yield() -- kill thread // assumes roblox thread fixed
			end
			
			print("working")
		end
	end)
	
	threads[curr] = true
	curr()
	
	return curr
end

local working = x()
wait(5)
threads[working] = nil

Also if wait is to be handled externally would it be like this?

-- assumes same interval for all just for brief example

local running = {}
local id = 0
local function createStepper(f)
	id = id+1
	running[id] = f
	
	return id
end

local function removeStepper(k)
	running[k] = nil
end

coroutine.wrap(function()
	while true do
		wait(0.1)
		for _,f in next, running do
			f()
		end
	end
end)()

local function step()
	print("working")
end

local working = createStepper(step)
wait(5)
removeStepper(working)

Btw

So in my 2nd post (before you posted)

local function myKill(thread)
	exists[thread] = nil
	if coroutine.status(thread) == "running" then
		coroutine.yield(thread)
	end
end

Is checking the status pointless? As in will it never return “running” because it will never be ran in parallel?

1 Like

No, I was thinking more along these lines:

local mGnr = coroutine.wrap(function(input)
    while input~=‘kill’ do
        print ‘running’
        input = coroutine.yield()
    end
end)
local t0 = tick()
while true do
    wait(0.1)
    if tick()-t0>5 then
        mGnr ‘kill’
    else
        mGnr()
    end
end
1 Like

That doesn’t satisfy the

1 Like

No, I think it should work fine since you’re calling wait().

Okay, but then you can just make multiple generators and keep track of when they were started. That’s just how I would do it, although I guess there are multiple ways to skin a cat. In general, I feel that it’s good practice to avoid a large number of concurrent loops, although ROBLOX’s task scheduler takes care of it all in the end.

local c = coroutine.create(function()
	wait(5)
	end)
print(coroutine.status(c))
coroutine.resume(c)
print(coroutine.status(c))
wait(5)
print(coroutine.status(c))

For first two it returns suspended and last it returns dead, when would it ever return running?
Also I think I was misusing wrap in some of my code above where I should have been using create (to get the thread) and then resume (instead of calling the function)


What do you mean by multiple generators though?
I’m having a hard time understanding how your code would scale without turning into my stepper with one universal loop example

If you don’t care about errors propagating from the coroutine to the main thread, then coroutine.wrap is fine; otherwise, use coroutine.create/resume, which calls the function in protected mode (like pcall).

As for when it would return running, presumably the only time that would happen is if you’re calling coroutine.status from inside that coroutine.

Well, I don’t see why there’s a problem with one universal loop. You could store the threads in a more specific manner if it suits you. For example, you could collect a list of threads into another thread which operates on their combined output, then pass that to a “universal loop”.
I assume the reason there’s not already a coroutine.kill method is that nobody really ever uses coroutines in that way.

I just don’t think it’d ever be a good idea to give every single thread its own loop using wait(). Personally I want more control over things like what order my threads run in, and how I handle their combined output. Also, if I only have one loop, then I can control the refresh rate with only one variable. I feel like a general coroutine.kill method should never really be necessary, but do correct me if I’m wrong.

1 Like

Coroutine “killing” is generally a bad idea. Depending on the implementation the coroutine could be doing anything at the moment it’s “killed”. Not to mention state and gc cleanup that also needs to happen (which I’m pretty sure simply yielding it will cause it to memleak). You’re better off having a failsafe inside the coroutine to jump out of the loop and let it die on its own.

5 Likes

Are non-yielding blocks of code split up by the task scheduler?

This message says it will be auto gc-ed:
http://lua-users.org/lists/lua-l/2005-08/msg00552.html

1 Like

Roblox uses their own thread scheduler so I can’t guarantee anything.

3 Likes

O ok
What about first question though?

Again, I’m not sure how they handle task scheduling. They shouldn’t behave that way, but it’s entirely possible they might.

1 Like

Here’s a way I used to kill a thread:

local function foo()
	while true do
		wait(0.1)
		print("working")
	end
end

local function kill(thread, f)
	local env = getfenv(f)
	function env:__index(k)
		if type(env[k]) == "function" and coroutine.running() == thread then
			return function()
				coroutine.yield()
				error("Killed " .. tostring(thread), 0)
			end
		else
			return env[k]
		end
	end
	setfenv(f, setmetatable({}, env))
	coroutine.resume(thread)
end

local thread = coroutine.create(foo)
coroutine.resume(thread)
print(coroutine.status(thread)) --> suspended
kill(thread, foo)
wait(0.1)
print(coroutine.status(thread)) --> dead

You should then be able to build your own manager and check if the thread running is blacklisted with a dictionary, instead of just coroutine.running() == thread.

10 Likes

What hapens when you don’t reference a global within the thread though?

E.g: add

local print = print
local wait = wait

to the beginning of the script (I always do it for speed)

2 Likes

It’s a workaround to inject the killing code, it won’t kill if you don’t reference global, that’s why you should have it’s environment set by your coroutine manager, or implement a function that does the same job within the function.

Since it requires referencing a global why would I want to use it over one of the previous solutions?

Your code would either crash or die if it doesn’t reference a global anywhere, most functions don’t localize everything, the thread can only be killed when it’s running, if you don’t want to add checks while it’s running, your coroutine manager should set the environment before it localizes.

What do you mean by set the environment before it localizes?
The stack is different from the static globals