This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
TheGeek's profile picture

[LUA] Queuing a time-delayed event

Started by TheGeek, 05 March 2013 - 01:11 PM
TheGeek #1
Posted 05 March 2013 - 02:11 PM
I have a large (nearly 2000 lines) program that I am working on, and I'm trying to solve a problem I am having.

I have a control system with a text-based GUI displayed on an advanced monitor. Using the "monitor_touch" event, I've created what I think is an Object Oriented GUI system, but thats actually working.

This system will occasionally need to run a series of time delayed commands that can "tie up" the system, leaving it unresponsive to input. I'd like to avoid this if at all possible. IE: using sleep([time]) makes the touch screen stop working while it waits.

I would like to implement a function or API that would allow me to queue a time-delayed event. I could use os.setTimer([time]), but that doesn't allow me to pass on any additional variables like os.queueEvent("event", param1, param2). There could be some instances where more than one time-delayed set of instructions could be running at the same time, so using os.setTimer() could get overly complicated.

Ideally the function would have the following parameters:
apiNameHere.queueDelayEvent("event", time, param1, param2…)

Is this feasible in LUA? Is there some function like this already? Or is there some better method to implement this?
Lyqyd #2
Posted 05 March 2013 - 02:47 PM
Use the timers, but also keep a table of the additional parameters. Use the ID the timer start call returns as the key for the table entry, then when a timer event is received, check your table and act accordingly.
shiphorns #3
Posted 05 March 2013 - 03:16 PM
It's unclear to me what you're really asking. You say that you need to run a series of commands that will "tie up the system" and leave it unresponsive to input, and you want to avoid this. But what's not clear to me is whether or not you are asking for a way to suspend execution of the time consuming tasks when I user interacts, or simply defer execution of the time consuming commands until some time when you know there won't be someone using the computer.

Using a timer is going to work for the latter, allowing you to defer the events that leave the system unresponsive. As Lyqyd suggests, you can use a hash table for the parameters–give your events unique IDs to key on and retrieve their parameters.

But, it sounds like what you might really want is to investigate using coroutines, so that you can effectively run your time-consuming code in the closest thing you're going to get to another thread, as a background task, so that it can yield time to the main application's loop. http://www.lua.org/pil/9.html
TheGeek #4
Posted 05 March 2013 - 03:18 PM
Use the timers, but also keep a table of the additional parameters. Use the ID the timer start call returns as the key for the table entry, then when a timer event is received, check your table and act accordingly.


It's unclear to me what you're really asking. You say that you need to run a series of commands that will "tie up the system" and leave it unresponsive to input, and you want to avoid this. But what's not clear to me is whether or not you are asking for a way to suspend execution of the time consuming tasks when I user interacts, or simply defer execution of the time consuming commands until some time when you know there won't be someone using the computer.

Using a timer is going to work for the latter, allowing you to defer the events that leave the system unresponsive. As Lyqyd suggests, you can use a hash table for the parameters–give your events unique IDs to key on and retrieve their parameters.

But, it sounds like what you might really want is to investigate using coroutines, so that you can effectively run your time-consuming code in the closest thing you're going to get to another thread, as a background task, so that it can yield time to the main application's loop. http://www.lua.org/pil/9.html

Whats the best method for keeping track of the timer ID? Is there any way to tell what ID the next timer event will use, or does it just increment from the last one? How do I get the first ID and increment from there?

Would it be possible to have a co-routine queue the event at the right time? That sounds like the solution I was looking for. What would such a function using coroutine look like?

When I mean "time consuming", i mean code that by design has to use lots of sleep(time) commands, as timing is critical to proper operation. Its not necessarily doing work in the background, just waiting.
Edited on 05 March 2013 - 02:20 PM
Grim Reaper #5
Posted 05 March 2013 - 03:30 PM
Whats the best method for keeping track of the timer ID? Is there any way to tell what ID the next timer event will use, or does it just increment from the last one? How do I get the first ID and increment from there?

Would it be possible to have a co-routine queue the event at the right time? That sounds like the solution I was looking for. What would such a function using coroutine look like?

When I mean "time consuming", i mean code that by design has to use lots of sleep(time) commands, as timing is critical to proper operation. Its not necessarily doing work in the background, just waiting.

Keeping track of a timer ID would be as simple as assigning it to a variable. However, I don't think that you can predict the next ID that will be used by you program.

It might be possible to use a co-routine to queue the event at the right time, but I don't think that those who are trying to help you (including myself) understand your problem in depth enough to suggest a way to implement corouitnes in your system.
TheGeek #6
Posted 05 March 2013 - 04:03 PM
Whats the best method for keeping track of the timer ID? Is there any way to tell what ID the next timer event will use, or does it just increment from the last one? How do I get the first ID and increment from there?

Would it be possible to have a co-routine queue the event at the right time? That sounds like the solution I was looking for. What would such a function using coroutine look like?

When I mean "time consuming", i mean code that by design has to use lots of sleep(time) commands, as timing is critical to proper operation. Its not necessarily doing work in the background, just waiting.

Keeping track of a timer ID would be as simple as assigning it to a variable. However, I don't think that you can predict the next ID that will be used by you program.

It might be possible to use a co-routine to queue the event at the right time, but I don't think that those who are trying to help you (including myself) understand your problem in depth enough to suggest a way to implement corouitnes in your system.

Definitely wouldn't want you to try to slog through my 2000+ line program, but perhaps a coroutine could be used to implement a function that would do what i want?
queueDelayEvent("event", time, param1, param2…)
Lyqyd #7
Posted 05 March 2013 - 04:53 PM
This is what I mean, roughly:


local tableOfStuff = {}
local timerID = os.startTimer(4)
tableOfStuff[timerID] = {"extra", "params", "here"}
while true do
  event = {os.pullEvent()}
  if event[1] == "timer" then
    if tableOfStuff[event[2]] then
	  --use tableOfStuff[event[2]] as the extra params
    end
  end
end
shiphorns #8
Posted 05 March 2013 - 04:55 PM
When I mean "time consuming", i mean code that by design has to use lots of sleep(time) commands, as timing is critical to proper operation. Its not necessarily doing work in the background, just waiting.

Yeah, a less abstract explanation of what you're actually trying to do would definitely help. I completely misinterpretted what you mean. I assumed you meant time consuming as in processor time, like you were going to be doing millions of calculations in loops that would yield time infrequently.

Now that I better understand what you mean, I wouldn't recommend a coroutine. It sounds like what you need is to use either os.startTimer() or os.setAlarm() (depending on if you want an arbitary amount of time in the future, or a specific in-game world time). Both of these functions return an ID for the timer they start, which you can than use as a key for a table entry with all the other data you wish to associate with that event, such as what function to call, what parameters to pass, etc. Heck, you could even make a closure out of the function and close over all the parameters right at creation time and stick that in your table keyed on timer ID.
shiphorns #9
Posted 05 March 2013 - 05:16 PM
Yes, what Lyqyd said is a good approach. In his example, if you want to print the word "extra" 4 seconds in the future, it might look like this:


local futureEvents = {}
local timerID = os.startTimer(4)
futureEvents[timerID] = {"extra"}
while true do
  event = {os.pullEvent()}
	if futureEvents[event[2]] then
	  print(futureEvents[event[2]][1])
	  return
	end
  end
end

And what I meant by using a closure/anonymous function in the table, to do the same thing, would look like this:


local futureEvents = {}
local timerID = os.startTimer(4)
futureEvents[timerID] = function() print("extra") end
while true do
  event = {os.pullEvent()}
	if futureEvents[event[2]] then
	  futureEvents[event[2]]()
	  return
	end
  end
end

In this case, you are effectively storing a function call with its arguments for future calling. You can even close over variables in your program that are available at the time you queue up the event, getting a snapshot of their values. Here's one way (probably there is a more elegant way, but I'm falling asleep…)


local someValue = "extra"
local futureEvents = {}
local timerID = os.startTimer(4)

do
  local arg = someValue
  futureEvents[timerID] = function() print(arg) end
end

someValue = "new value"

while true do
  event = {os.pullEvent()}
  if event[1] == "timer" then
	 if futureEvents[event[2]] then
	  futureEvents[event[2]]()
	  return
	 end
  end
end

Notice that this still prints "extra" even though someValue is assigned "new value" right after the event is queued up. This is because the closure is around arg, which is only in scope during the do..end block.


I added the return statements just so you can run the example and not have to Ctrl-T out of it.
TheGeek #10
Posted 06 March 2013 - 11:36 AM
Sounds like the solution I was looking for.

One quick question though: Is there any danger of a memory leak? IE: will the table get so large from long term use that it will take up large amounts of memory on my server (that doesn't have all that much memory to begin with)? Would there be any need to have a "table flush" function that say would make table = {} every so often?

Thanks for all the great help so far!
Lyqyd #11
Posted 06 March 2013 - 12:38 PM
Just nil the index of the table after it has been used. In my original example, in the if block containing the comment, you could place this at the end of the if block to alleviate any memory concerns:


tableOfStuff[event[2]] = nil