ID:151625
 
Let's say you have a world event that goes off every 1800 seconds. How do you like to perform these loops? Here are some methods I've seen used over the years.

Method 1
WorldEvent()
while(1)
sleep(18000)
// execute some code


Method 2
WorldEvent()
sleep(18000)
// execute some code
spawn()
WorldEvent()


Method 3
WorldEvent()
for()
sleep(18000)
// execute some code


Method 4
world.New()
..()
spawn()
while(1)
sleep(18000)
// execute some code


Method 5
world.New()
..()
spawn(18000)
WorldEvent()

WorldEvent()
spawn(18000)
WorldEvent()
// execute some code


My preferred method is #5 for a very specific reason. Even though you have to duplicate the time in two locations (could be solved with a var/#define), it provides one clear benefit over other methods. If any of the other methods receive a runtime error they will immediately die, and the method will never be executed again. With method #5, this doesn't happen, because the timer for the next iteration is always kicked off before any errors can occur.

Let's say you had a loop with some kind of reference to a mob's client in a multiplayer game. If this player disconnects and you have a deadlinked mob, if you haven't accounted for this error then it will crash your procedure. In this way, method #5 protects you against most "oops, I missed that" programming errors that you can make - therefore this is very useful for early testing phases of a game. As for efficiency, I've never seen any compelling reason to switch it unless a loop is going off once a tick or similarly fast.

What are your thoughts?


~Polatrite~
All work, of course. "Method 3" is the most 'right' way. Constantly restarting the same proc is a relatively poor method to do it - there's no reason to incur the extra overhead of calling a function when all you want to do is loop the same bunch of statements, and, of course, it's limited since you can't easily loop parts of the proc with it, rather than all of it. So unless you need recursion, to loop properly, you should use a... proper looping mechanism. Even explicitly writing goto start is better IMO than spawn() .(); both are essentially the same, except the latter does more work.
That leaves you with for() and while(), and because you want an infinite loop that has no condition to keep on going based on, and only for() supports conditionless loops, that leaves for() as the best choice. (Of course, you can work around while() requiring a condition by giving it a dummy, constantly-true condition to pointlessly check every iteration, but that's a workaround versus a feature that naturally allows what you want.)
BYOND handles sleep() and spawn() through the same system, which basically puts procs into a list of procs that need to be run at some point in the future (or on the current tick). So most methods are usually equivalent. Doing a sleep() followed by a spawn() is pretty pointless though, so method 2 should be thrown out. For methods 1 and 3 you also need to be aware that whatever routine called WorldEvent() is going to be stuck waiting for the proc to finish, which it never will.

If you anticipate using a crapload of sleeping/spawned procs though, you may be better off using an event manager, which can keep track of which procs still need to be run in a simple queue. The queue would have random-access insert so you could add an event at any point, but would always run events in order.

With an event queue the main danger to watch out for is when an event needs to be moved up, or when an event is added ahead of all others in the queue. During this time the event loop should be sleeping, so you basically have to use a sequence number to tell the sleeping loop to give up, and start a new loop. It'll look something like this:

event
var/time
var/object
var/procname
var/a,b,c,d
var/isglobal

New(delay,object,procname,a,b,c,d)
src.object = object
src.procname = procname
src.a = a
src.b = b
src.c = c
src.d = d
time = world.time + delay
isglobal = !object
eventloop.Add(src)

proc/Fire()
if(object || isglobal) call(object,procname)(a,b,c,d)

eventloop
var/list/events = new
var/id = 0

proc/Add(event/E)
var/i,first,last
first = 1
last = events.len
while(first <= last)
i = round((first+last) / 2)
var/event/Ecmp = events[i]
if(Ecmp.time <= E.time) first = i + 1
else last = i - 1
events.Insert(first, E)
if(first == 1) SpawnFire()

proc/Remove(event/E)
events -= E

proc/Update(event/E)
Remove(E)
Add(E)

proc/SpawnFire()
if(!events.len) return
var/current_id = ++id
var/event/E = events[1]
spawn(E.time - world.time)
if(current_id == id) // is this still current?
while(events.len)
var/event/E = events[1]
if(events[E] > world.time) break
events.Cut(1,2) // remove event from queue
spawn(-1) E.Fire()
if(events.len) SpawnFire()

var/eventloop/eventloop = new // global object


The advantage of that system is that only one proc will be sleeping at a time, as opposed to possibly hundreds. As you see the event loop uses a binary search to decide where to place a new event in the queue. If it adds the event to the head of the list it's because it needed to happen before any other event currently pending. If the event list already had entries then a SpawnFire() proc is currently sleeping. If you're adding an event for a later time, it can simply add that to the queue and let SpawnFire() handle it later.

We need the id# thing because sometimes we may want to kill the current SpawnFire() that's sleeping and launch another one. That happens if you're adding an event that's earlier than the first one on the list. The new SpawnFire() gets a new id#. Once the old one finally wakes up, it'll realize it's outdated and simply terminate.

Lummox JR
Method 6
WorldEvent(recursive)
// execute some code
if(recursive)
spawn(180000)
WorldEvent(recursive)


Method 6 is what I use. I would avoid method 5 in most situations because the body of the function may contain sleeping functions, and I wouldn't want the next iteration to start before the current one finished. The most basic reason for using 6 over 5, however, is because it is the most understandable. Having understandable program structure will net you many more performance gains, in the long run, than nit-picking and demonizing function calls.

In most cases, a recursive function (using spawn) is the best call. The reason is that these functions are generally not loops from a design perspective. If the design calls for an "update to user statistics every 2 hours", what I have is a discrete event (update user statistics) which happens every two hours. Discrete events are represented programmatically by functions. This is to ensure that we can use that same event elsewhere. Perhaps the death of a player warrants an update to the user statistics, too.

Now, you have to call this event somehow, so you could do that in a loop. However, any performance gained by using the loop instead of a recursive proc has now been lost (because we're calling the proc from inside the loop, anyway). In fact, any notion of performance gain by shunning procs is false efficiency. There's a reason why programming has moved away from Procedural practices and on to Object Oriented practices.
Another benefit of method #5 is that the next iteration is scheduled to start a certain amount of time after the *start* of the previous iteration (as opposed to the end). In all of the other examples the time it takes to execute the code is also counted towards the delay. For example:
looping_proc()
while(1)
world << world.time

// do some calculation that takes 2 ticks

sleep(10)

Assuming you wait for 10 ticks before the loop begins, the output will be: 10, 22, 34, 46, etc.

Using this method, the first iteration will start at tick 10 and schedule the next iteration to start at tick 20 (no matter how long the iteration takes):

looping_proc()
spawn(10) looping_proc

// do some calculation that takes 2 ticks


Of course, there are ways to handle this with the other methods:

looping_proc()
while(1)
var/start_time = world.time

// code

var elapsed = world.time - start_time
sleep(10 - elapsed)


Most other issues will make no difference. If you're worried about the overhead of a while loop, you're paranoid. If you're truly that paranoid, you'd use goto:

looping_proc()
start_of_looping_proc:

// code

sleep(10)
goto start_of_looping_proc


Most people who are paranoid about the overhead of while loops or function calls are the same people who are paranoid about the use of goto, but that's another issue.

If you have a lot of periodic events, you might need a certain implementation (like [link]). If you have one event with a short period, performance may be more of a concern. Other times, the interval may not be short but timing must be precise. Other times, you may have a single event with a long period so it doesn't really matter how you handle it. The optimal implementation depends on what you're using it for so without a specific context there is no right answer.
In response to IainPeregrine
IainPeregrine wrote:
Method 6 is what I use. I would avoid method 5 in most situations because the body of the function may contain sleeping functions, and I wouldn't want the next iteration to start before the current one finished.

This is certainly an interesting consideration - especially important when dealing with operations that have extended performance hits. I remember back in 2002 (or something) when I originally programmed Survival, I was working on a map saving routine (a concept only modestly explored on BYOND at the time) to save all the contents of the world. When the game had many players online and was running object counts of 6000-7000+, the save procedure actually caused the game to lag for a couple seconds every 5 minutes when the method was called.

I switched to using a delayed save (sleep()s inserted at the end of the saving loop) in order to alleviate this problem - only to find out that I had delayed it TOO long and it was taking over 5 minutes to perform the entire save. Then the file would get overwritten by itself and lead to the corruption of data. I then also realized that the game state could shift too much during the period of time that the players themselves could cause "corruption" of the data (by entirely deleting or duplicating items), so it was reverted. The lag simply became part of the game until I rebuilt the game from the ground up in 2008-2009.

So previous executions is definitely a concern when using operations like this, and it's a good thing you detailed that.



IainPeregrine wrote:
Now, you have to call this event somehow, so you could do that in a loop. However, any performance gained by using the loop instead of a recursive proc has now been lost (because we're calling the proc from inside the loop, anyway). In fact, any notion of performance gain by shunning procs is false efficiency. There's a reason why programming has moved away from Procedural practices and on to Object Oriented practices.

I entirely agree with this statement. And it caused me to think of yet another benefit for recursive method calls - profiling. You can't accurately profile the iteration of a while() or for() loop with the DM profiler (in fact, you can't do it with the VS 2003/2005/2008 profiler either).

I think the overhead from a method call is almost entirely negligible unless your event loops involve hundreds of calls per ticks. If you're that concerned about performance, I'd recommend not using a high-level language at all.



Lummox JR wrote:
BYOND handles sleep() and spawn() through the same system, which basically puts procs into a list of procs that need to be run at some point in the future (or on the current tick). So most methods are usually equivalent. Doing a sleep() followed by a spawn() is pretty pointless though, so method 2 should be thrown out.

Method 2 actually won't work if you don't have the spawn() - the method itself will never return and you will eventually get a stack overflow.

Does Deadron's Event Loop library correspond with the functionality you detailed in your post? I have only briefly looked at the library, but I recall seeing a BYONDscape article about it once upon a time.



Forum_account wrote:
Another benefit of method #5 is that the next iteration is scheduled to start a certain amount of time after the *start* of the previous iteration (as opposed to the end).

This is another great reason for method 5 - important when events need to happen exactly on time (every 5 minutes; every hour on the hour) but the effects of the events may take a little while to execute, and it's cleaner than spawning THE METHOD to another psuedo-thread, and instead spawns the recursive call (which makes more sense to me, and releases the method to return properly at the end).


~Polatrite~
In response to Forum_account
Constructs like while() and for() aren't real statements, they're just nice constructs for you to use to do looping without creating horrible spaghetti code. They're all compiled to goto statements (well, instructions such as jump and jump_if_true).
while(Condition)
Statement

//is compiled to something like
:loop123
if(!Condition) goto loop123_exit
Statement
goto loop123
:loop123_exit

The difference between using goto and while() is in the source code produced, and as known, goto is loathed for the unreadable code it produces, thus those constructs were created to produce easily-readable loops (how someone finds them less readable than suddenly restarting the proc later on, I have no idea).

By the way, nobody is "paranoid about the overhead of while loops or function calls [or] use of goto" (especially since most of what you mentioned there doesn't exist). The points I made are about intent and purpose; also note the OP asked for 'the most right way' for infinite loops, which I supplied. If you want to loop a bunch of statements, the answer is to put them in a loop, rather than a roundabout recursive proc. If you want the loop to have no condition, the answer is to use a looping construct that doesn't require a condition, rather than use one that does and workaround that by making the condition 1. Obviously enough, sticking random if(1) statements in your code will never cause any noticeable performance drop, but still, people don't do so, for some weird reason. That reason is that they don't need to.
In response to Kaioken
If you are going to keep on railing on about how amazing for() is and blah blah blah and while() is dumb and "LOL if(1)" I am just going to have to slap you.

I prefer or while(src) or while(true) because it is explicitly obvious what it means to anybody reading it what is happening, without even needing (much) prior experience. for(), on the other hand, is not so explicit. for() what?

It is in no way analogous to if(1) because if(1) does not serve the purpose of being more readable.

If you prefer for(), then good for you. Have fun with it. But goddamn, shut up about it already.
In response to Lummox JR
I was waiting for the invoker part of the command pattern to appear. I do recommend something like this, particularly for the benefit of also allowing event notification. All interested parties register their interest on the event queue for some type of event, and receive notification of one such event firing, probably with an object (maybe the event if it's small, maybe something else) that encapsulates some state pertaining to the event. The nice thing about that is the ability to delegate to any number of interested parties without needing to hold reference yourself to them. It's kind of a reverse of the norm for OO, in that you are pushing state / changes to processors, instead of processors fetching it when they need.
In response to Garthor
Garthor wrote:
If you are going to keep on railing on about how amazing for() is and blah blah blah and while() is dumb and "LOL if(1)" I am just going to have to slap you.

So they finally invented a way to slap people over the internet? Fill me in, damn, I could use that.

I prefer or while(src) or while(true) because [...]

You can feel free to prefer whatever you want. I prefer while("loop foreva yo!"). However, the OP asked to compare between a number of different methods, not whichever is your personal favorite. Thus, I answered that the most suitable loop method for one that doesn't need to check any condition is one that doesn't check any condition, which I think is quite objective. Had he asked for my personal favorite, I would've gave him the one mentioned, since I believe it adds color to your code. Seems straightforward to me.

It is in no way analogous to if(1)

Really? I was under the impression that if() and while() both check if what's inside the parentheses is true. My bad. :X

because if(1) does not serve the purpose of being more readable.

Hmm, now that you mention it, it's possible to try and use it in such a manner, but I wonder where you've brought that from.
In response to Kaioken
Kaioken wrote:
I prefer or while(src) or while(true) because [...]

You can feel free to prefer whatever you want. I prefer while("loop foreva yo!"). However, the OP asked to compare between a number of different methods, not whichever is your personal favorite. Thus, I answered that the most suitable loop method for one that doesn't need to check any condition is one that doesn't check any condition, which I think is quite objective.

No, that is untrue, because if you were concerned with nothing but efficiency your answer would be to simply use goto. After all, why would you want an exit label if you weren't ever planning on breaking the loop? That's obviously inefficient!
In response to Garthor
Hey, stop flaming on my thread, you flamers.

Or at least flame me, geez. Can't even flame a man in his own thread. What's the world coming to?


~Polatrite~
In response to Kaioken
goto is loathed for the unreadable code it produces

I wouldn't say that, but I would agree that goto is avoided for the unreadable code it can produce. The point is not to avoid goto but to avoid writing unreadable code. It's important to remember this because you can still write unreadable code without using goto.

It is possible to procude unreadable code by using for loops in situations that are traditionally handled with while loops. Despite being completely interchangeable, for loops are commonly used to iterate over bounded ranges and while loops are commonly used to repeat actions while a condition is held. Being able to omit the three parts of a for loop is a rather DM-specific quirk. While you might find it readable, you won't find many people who agree.

also note the OP asked for 'the most right way' for infinite loops, which I supplied.

Without knowing how Polatrite is going to use periodic events, nobody can determine which method is most correct. We can talk about the benefits of some methods and the flaws of others, but there is no "most right way".

Getting back on topic:

Another benefit of calling sleep/spawn after the body of code has executed is that you can adjust the delay (or stop future events) based on the work done by the rest of the code. Depending on the situation, you may spawn the next call at the start of the event only to realize that you want the event to stop. Once you've called spawn you can't take it back.

One other thing to consider is how you stop the event. Once you've spawned the next call you can't take it back (or can you). You could try to coordinate things with global flags, but this can have problems too.

By containing the event in an object you can stop the event by deleting the object. For example:

PeriodicEvent
var
period = 10
New(p)
period = p
spawn() go()
proc
go()
// do something
spawn(period)
go()
mob
Login()
var/PeriodicEvent/p = new /PeriodicEvent(10)
sleep(35)
del p


The alternative would probably be some error-prone method that uses global flags to signal the event to stop spawning more calls. This is much cleaner. Whatever method you prefer (sleep/spawn/etc.) there is a distinct benefit to using objects.
In response to Forum_account
Forum_account wrote:
The alternative would probably be some error-prone method that uses global flags to signal the event to stop spawning more calls. This is much cleaner. Whatever method you prefer (sleep/spawn/etc.) there is a distinct benefit to using objects.

Yeah, I use datums for Action Control as it's far more reliable when events are called based on user input (as opposed to a bounded, reliable system call).

Pretty old library though. I'd write it a little bit differently now, but I like it.


~Polatrite~
In response to Forum_account
Forum_account wrote:
goto is loathed for the unreadable code it produces

I wouldn't say that, but I would agree that goto is avoided for the unreadable code it can produce.

I'm not willing to discuss tiny semantics. At any case, many will disagree with you - you will find that some people will instantly condemn you for merely bringing up the option of using goto. Happened to me right on these forums, I believe.

It is possible to procude unreadable code by using for loops in situations that are traditionally handled with while loops.

I don't think so, especially as in DM you can convert while(X) directly to for(,X) with the exact same effect. If you think of it that way, while() is even superfluous, and is included for developers who are used to it from other languages.

Being able to omit the three parts of a for loop is a rather DM-specific quirk.

And your point is? People use DM-specific quirks in DM all the time, even undocumented ones. Yet when using a for() loop with no condition comes up, which is even documented, it's suddenly an exception and people act as if you've insulted their religion, because while(1) is their favorite method and they worship it.
BTW, just so you're aware, using a language X-specific quirk in language X is "kinda" part of using the language, surprisingly enough. Many people choose to use DM primarily of its 'weird quirks' that make it easier to use, such as parsing indentation and not requiring fugly braces.

While you might find it readable, you won't find many people who agree.

Like some built-in procs, it's only readable once you learn about it. Obviously some names like "image","orange","Topic" don't mean anything specific to someone that doesn't already know what they do. "while one" and "for" aren't particularly different.

Without knowing how Polatrite is going to use periodic events, nobody can determine which method is most correct.

He merely asked how you should make 'persistent loops', or infinite loops, not how to implement event or feature X. It appeared to me like a generic, simple question. He didn't talk about anything specific, like what he will do in the loop or what it's actually for, so that's all out of the question.
In response to Kaioken
Kaioken wrote:
Yet when using a for() loop with no condition comes up, which is even documented, it's suddenly an exception and people act as if you've insulted their religion, because while(1) is their favorite method and they worship it.

No, it's when you state that "objectively" for() is correct and everybody else is incorrect that people start getting really annoyed at you

ALTERNATIVE REPLY:

Yet when using a while() loop with an always-true condition comes up, which is even documented, it's suddenly an exception and people act as if you've insulted their religion, because for() is their favorite method and they worship it.
I've tricked everyone by implying there is a "right way" to do persistent loops.

I really just wanted diversity in methods, and had hoped people wouldn't get hung up on back-and-forth argument about the same two methods.

I'm still using method #5 until I have need of a more complex solution, at which point I'll bite the bullet on the Lummox JR/Deadron solution.


~Polatrite~
In response to Garthor
I never stated that using while() for conditionless loops is incorrect. Only that for() is more correct, which I don't particularly see as something you can argue about, as it was built with it in mind, and the alternative wasn't and requires a workaround (which happens to be a known, commonly used and harmless one, but still a workaround) to work as desired. Of course, if you don't look at it objectively, you can somehow miss this simple fact. Good thing I don't subjectively present while("infinite loop yo!!!!!111") as the most appropriate solution.
In response to Kaioken
for() is not more correct in that it is less clear as to its function than while(1) or while("infinite loop yo!!!!!111"). This is the exact same reason that goto is not more correct than for(), despite being designed with infinite loops in mind. If you are ignoring the quality of being easily understandable, then you should logically be advising the use of goto.
In response to Kaioken
Kaioken wrote:
I never stated that using while() for conditionless loops is incorrect. Only that for() is more correct, which I don't particularly see as something you can argue about, as it was built with it in mind, and the alternative wasn't and requires a workaround (which happens to be a known, commonly used and harmless one, but still a workaround) to work as desired. Of course, if you don't look at it objectively, you can somehow miss this simple fact. Good thing I don't subjectively present while("infinite loop yo!!!!!111") as the most appropriate solution.


Something can't be "more correct" than something else.. If it's correct, it's correct. Maybe "more efficient" or "smarter to use" but not more correct..
Page: 1 2