Run new thread immediately after current one yields

coroutine.wrap(function()
	print'new'
end)()
print'cur'
coroutine.yield()

Basically how can I make it so ‘new’ outputs after ‘cur’ WITHOUT using some form of wait()? Its fine if the new thread is the last one executed in the current scheduler cycle (as opposed to running immediately when the current thread yields) but I do not want it to wait until the next cycle starts before running (as spawn does)
Currently resuming a thread will always push it to the front to execute and wait for it to yield before continuing with the rest of the task scheduler’s tasks (which in most cases is desired)

My specific use case is that I have a signal module which whenever some sort of change is made fires its own Changed remote (this definition is not recursive though xd)

In my network module I wait for the remote’s corresponding signal to be Bind-ed or Wait-ed to before unloading the accumulated data (Roblox does this internally too but their queue size is 250, I ran into a scenario where I needed more so I had to make my own queue)

In my Signal module’s Wait, Changed is fired BEFORE the thread yields, causing the Network to unload the queue without actually resuming the Wait
If I could instead first yield and then call Changed everything would work perfectly

Here’s the relevant code snippets:
In Signal.Wait:

if sig.Changed then
	sig.Changed('Wait',t)
end
	
return coroutine.yield()

In Network:

r.Changed:Bind(function(status)
	if status=='Bind'then
		while q.N>0 do
			fire(r,unpack(q:Pop()))
		end
	elseif status=='Wait'and q.N>0 then
		fire(r,unpack(q:Pop()))
	end
end)

Of course this could be solved with a wait() but that is messy so I’d like to avoid it

Thanks

2 Likes

What do you intend to do before calling Changed, that is, what is required to “first yield and then call”?

I thought I had a good idea here

local function last(f)
	getmetatable(newproxy(true)).__gc = f
end

print("a")
last(function()
	print("c")
end)
print("b")

but it seems Roblox disabled __gc because it ran in a higher security context.

1 Like

If it were still supported, would we be guaranteed the gc is aggressive enough to catch it in the next sleep cycle? (Sleep cycle might be the wrong term here)

If it unloads before the thread yields, I lose data

Here’s how you can connect a custom event:

local event = {}
event.__index = event

function event.new()
	return setmetatable({Connections = {}, Waiting = {}}, event)
end

function event:Fire(...)
	if #self.Connections > 0 or #self.Waiting > 0 then
		if self.Queue then
			local queue = self.Queue
			self.Queue = nil
			for _, request in pairs(queue) do
				self:Fire(unpack(request))
			end
		end
		for connection in pairs(self.Connections) do
			coroutine.resume(connection, ...)
		end
		for connection in pairs(self.Waiting) do
			self.Wait[connection] = nil
			coroutine.resume(connection, ...)
		end
	else
		self.Queue = self.Queue or {}
		table.insert(self.Queue, {...})
	end
end

function event:Connect(callback)
	coroutine.wrap(function()
		local thread = coroutine.running()
		self.Connections[thread] = true
		while self.Connections[thread] do
			coroutine.wrap(callback)(coroutine.yield())
		end
	end)()
end

function event:Wait()
	local thread = coroutine.running()
	self.Waiting[thread] = true
	return coroutine.yield()
end

function event:Disconnect()
	self.Connections = {}
	self:Fire()
	self.Waiting = {}
end
1 Like

Your :Wait() does not resume immediately if there is an item in the queue, but I guess this could be easily solved with an if statement there

But wow; I can’t believe I missed this, I guess it’s because I didn’t think about implementing it inside of the signal: for me my signal didn’t hold a queue, I added that functionality in my network module while still using the old signal methods

Well I’m just very happy about this, thank you

function event:SolveQueue()
	local queue = self.Queue
	self.Queue = nil
	for i=#queue, 1, -1 do
		self:Fire(unpack(queue[i]))
	end
end

function event:Fire(...)
	if #self.Connections > 0 or #self.Waiting > 0 then
		if self.Queue then
			self:SolveQueue()
		end
		for connection in pairs(self.Connections) do
			coroutine.resume(connection, ...)
		end
		for connection in pairs(self.Waiting) do
			self.Wait[connection] = nil
			coroutine.resume(connection, ...)
		end
	else
		self.Queue = self.Queue or {}
		table.insert(self.Queue, {...})
	end
end

function event:Connect(callback)
	coroutine.wrap(function()
		local thread = coroutine.running()
		self.Connections[thread] = true
		while self.Connections[thread] do
			coroutine.wrap(callback)(coroutine.yield())
		end
	end)()
	self:SolveQueue()
end

function event:Wait()
	if self.Queue then
		return unpack(table.remove(self.Queue))
	else
		local thread = coroutine.running()
		self.Waiting[thread] = true
		return coroutine.yield()
	end
end
1 Like