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

Avoiding "Too long without yielding" - The efficient way

Started by MysticT, 18 March 2013 - 09:06 AM
MysticT #1
Posted 18 March 2013 - 10:06 AM
Most people has run into this problem when creating infinite (or really long) loops. And by now, almost everyone knows how to avoid it, the simple "sleep(0)".
But, is that the best way? Well, that depends on the situation, but in most cases the answer is no.
So, what other ways are there?
1- os.pullEvent
In some cases, you don't really need to keep looping, but just wait for an specific event, that's where os.pullEvent is used.
Example:
Imagine you need a program that does something every time there's a redstone signal on it's left. A simple way to do this would be:

while true do
  if rs.getInput("left") then
	-- do something here
  end
  sleep(0)
end
But this would just make the computer check, the yield to avoid an error, and check again, no matter if there was a change or not.
Now, we don't need to check all the time, just when there's a change. So, a better way would be:

while true do
  if rs.getInput("left") then
	-- do something here
  end
  os.pullEvent("redstone") -- wait for a change in redstone
end

2- os.queueEvent + os.pullEvent
This one is usefull if you don't need to wait for an specific event, or you need the loop to keep running.
It's almost the same as using sleep(0), since it will queue an event and pulls it. The difference is that this won't use a timer. Now, you may think, if I call sleep(0), the timer will wait 0 seconds until it fires. Well, it won't. Timers actually have a minimum time, which is a tick (or 0.05 seconds). This is not a really big difference, but it can be for really long loops.
So, here's a function that would queue a fake event, and yield until it pulls it:

local function yield()
  os.queueEvent("someFakeEventName") -- queue the event
  os.pullEvent("someFakeEventName") -- pull it
end
It will resume almost instantly, depending on how many computers are running.
Now, you just have to use it in a long loop that takes a long time:

for i = 1, someReallyBigNumber do
  doSomeCalculations(someTable[i])
  yield()
end
And it will avoid a "too long without yielding". But this can be really slow, specially if there's more computers running.
So, how can we make this faster? Well, we can't make it resume faster, but we can make it yield only when needed.
Lets make a better yielding function:

local yieldTime -- variable to store the time of the last yield
local function yield()
	if yieldTime then -- check if it already yielded
		if os.clock() - yieldTime > 2 then -- if it were more than 2 seconds since the last yield
			os.queueEvent("someFakeEvent") -- queue the event
			os.pullEvent("someFakeEvent") -- pull it
			yieldTime = nil -- reset the counter
		end
	else
		yieldTime = os.clock() -- store the time
	end
end
There are other ways to do it, but this should work.

Now your big loops won't generate lag on other computers and maybe will be even faster.
Hope this helps someone. Any feedback and comments are welcome.
PixelToast #2
Posted 18 March 2013 - 03:30 PM
the code at the end is verry helpful, thanks
immibis #3
Posted 18 March 2013 - 04:38 PM
You shouldn't really be doing 5-second-long calculations in CC…
PixelToast #4
Posted 18 March 2013 - 06:10 PM
You shouldn't really be doing 5-second-long calculations in CC…
mandlebrot takes forever to render :s
KaoS #5
Posted 18 March 2013 - 07:23 PM
shouldn't it be


local yieldTime=os.clock()
local function yield()
  if os.clock()-yieldTime>2 then
    os.queueEvent("yield")
    os.pullEvent()
    yieldTime=os.clock()
  end
end

otherwise it will never yield the first time you run it and then the second time it will yield and make yieldTime=nil, you can then wait 10 seconds, call yield again and it will just set yieldTime to be the time as it is nil…
MysticT #6
Posted 19 March 2013 - 05:46 AM
shouldn't it be


local yieldTime=os.clock()
local function yield()
  if os.clock()-yieldTime>2 then
	os.queueEvent("yield")
	os.pullEvent()
	yieldTime=os.clock()
  end
end

otherwise it will never yield the first time you run it and then the second time it will yield and make yieldTime=nil, you can then wait 10 seconds, call yield again and it will just set yieldTime to be the time as it is nil…
That's another way to do it. It's shorter and it would be better for most programs.
I just posted that way because I already had it (too lazy to rewrite a new one :P/>).
I did it that way because it was in an api. So, if you store the time when you load the api, it will probably yield in the first call. Also, if you call a function from the api with a loop that uses this yield, and then call it again after some time, the time stored will be the one used before, and again would yield on the first call.
It's a really small difference. But I like to do things as efficient as I can, no matter how small is the difference :P/>

You shouldn't really be doing 5-second-long calculations in CC…
Sometimes you have to XD
I did it in my nbs api, since loading long songs can take more than 10 seconds.
But yeah, most programs won't need to do it.
PlowmanPlow #7
Posted 21 March 2013 - 06:47 AM
I'm new to LUA and CC, but I've been doing my "catch specific events and still have a timer" thusly:


local displayTimer = os.startTimer(3)
while true do
   local event, p1, p2, p3 = os.pullEvent()
   if event == "key" and p1 == 16 then
	  return
   elseif event == "timer" and p1 == displayTimer then
	  updateDisplay() -- or whatever
	  displayTimer = os.startTimer(3)
   end
end

This would wait for you to press "Q" and run that display function every 3 seconds. Using the timer ID number is handy because there are other things that start their own timers (sleep(), rednet.receive(), etc.) and you don't want those timers triggering pieces of your code. This way any other timer events are silently ignored.