Queue

From TinyMUX
Jump to navigation Jump to search

Command Queue

The command queue (or often just "the queue") is where all action lists are put before execution. The command queue is a normal queue implementation: first in, first out (FIFO). The first action list that is entered into the queue is executed first. The last action list that is entered into the queue is executed last. Think of it like a long line at a ticket office which only has one window.

But.. Why?

MUSHes run as single process on a host machine. They are not typically multi-threaded, so the process can only do one thing at a time. However, one does not want to stop accepting input just because a single action list is tying up the MUSH for a second. So, instead, a queue is used. Action lists that need to be executed are put onto the queue, and the MUSH will execute them as soon as it can.

Also, if action lists are sufficiently quick to execute, the effect can look a good deal like multitasking. That is, one or more items of code may appear to be executed at once, and this is far more desirable than waiting for a piece of softcode to start.

Queues are also a convenient data structure for implementing waiting (perhaps via @wait) and invocation of additional code (via @trigger, $-commands, @wait, @switch, @ifelse, @force, @dolist, @program, etc).

It is important to realize that all of these commands put action lists onto the queue. None of them contain any code from a MUSH perspective until such code is placed onto the queue. This is especially good to keep in mind when using @switch and other types of queueing commands that depend on conditions. If the conditions on which a @switch depend change between the execution of the newly queued code and the evaluation of the @switch condition, unexpected situations may arise. This has often been a problem in CharGens and other intricate code.

Server Differences

There are two queue designs. The one used by PennMUSH, RhostMUSH, and TinyMUSH goes back and forth between the network and queue on a regular basis. Under light load, the period of this polling is between 0 and a little over 1 second depending on work contained in the queue and network traffic. This cycle can stretch out to longer than 1 second with more expensive softcode, but that's desirable.

During a queue cycle, the network is checked for activity, regular tasks are performed (dump checks, consistency checks), and commands in the queue are executed if necessary. Commands from objects are delayed by 1 queue cycle to give network commands a higher priority and allow a player a better chance of @halt'ing runaway objects. Also, MUSHes have historically run on University servers that were purchased under a different justification than running a MUSH, and in such circumstances, not showing up on a process report to the system administrator is more important than being efficient.

The fundamental unit of time in PennMUSH, RhostMUSH, and TinyMUSH is 1 second with a few calculations performed in milliseconds

The TinyMUX queue design is very different and has been since TinyMUX 2.0. There is no regular cycle and TinyMUX can and does remain dormant for an arbitrary but appropriate amount of time. It always keeps an eye on when the next task should be performed, does tasks that should run in the right order, and looks at the network whenever it has run out of tasks with a timeout based on when the next tasks will be performed. So, the regular polling cycle of the older queue design is gone. Regular tasks like dump checks, idle checks, and consistency checks have been turned into system-priority mini-tasks that run on a scheduled basis.

One way to visualize the difference between the two designs is that TinyMUX has added a mini-task layer below the normal queue.

Commands from the network continue to enjoy priority over commands from objects, but it doesn't need to use a 1 second queue penalty to accomplish this. It is also capable of using 100% of the CPU without offering any evidence of lag to network connections. Also, all the regular tasks mentioned above are scheduled in the same way that commands are scheduled. Nothing runs unless it has been scheduled.

The fundamental unit of time in TinyMUX is in units of 100ns.

Coding with the queue

Recent softcoding tendencies seem to involve using a structure for almost all code such as:

$+command *=*:@pe %#=lots of functions and usually side effect functions

It is as if using the queue is a lost art.

As the speed of machines increases, this is less of a concern because the need for virtual multitasking is lessened. However, there are still cases which crop out where multitasking is highly desirable to work towards. A common scenario is a census or MUSH-wide searching piece of code, often implemented with a search() function call and a large and relatively complex restriction argument. This code often freezes up the MUSH for five seconds to a few minutes. People may argue that this is minor if only run once a week, but if in the middle of a scene, this is very disruptive.

Function calls do not multitask. This is why there's severe default limits placed on the number of function calls that can be invoked. This helps prevent any one piece of code from monopolizing the MUSH for an arbitrary amount of time. However, special cases still occur. One is search(). Search() has the capability to bypass this limit and is thus restricted to Wizards (typically).

Instead, one can choose to emulate @search by manually sweeping the database. This can usually be done by looping. While such code may actually take a longer amount of processing and execution time in the long run, at no point will it hinder creativity or present annoyance to users.

Examples

Example #1

> @force me={@force me=say Second;say First}
You say, "First"
You say, "Second"

While this may seem counter-intuitive, consider what happens from a command queue point of view. @force enters an action list into the queue with an executor of 'me' (i.e. %!). That action list is: @force me=say Second;say First

That action list eventually is executed. There are two commands in that action list:

@force me=say Second
say First

Keep in mind that the first command is a @force, not a say. So, the first command is executed first (queue, FIFO): Another entry is made on the queue containing an action list with a single command: say Second But we're not done with this action list yet. We still have a second command to execute: say First So, we execute the second command:

You say, "First"

Now we move onto the next item(s) on the queue... Eventually we hit say Second:

You say, "Second"

Example #2

> @create Buzzer
> &c.buzz buzzer=$buzz:@switch v(d.%#)=1,
  @pemit %#=You have already hit the buzzer.,
  {@emit %n hits the buzzer!;&d.%# me=1}

The intention of this code is to allow objects of unique #dbref to user the buzzer exactly once. (Note my careful wording about dbrefs)

> @fo me={buzz;buzz;buzz}
Whoever hits the buzzer!
Whoever hits the buzzer!
Whoever hits the buzzer!

That certainly wasn't the intended result. What's going on here? Well, @force queues in the action list:

buzz;buzz;buzz

Each 'buzz' queues in the action list:

@switch condition=...,condition,set some data that affects the condition

Each of these three @switches now queues in either the error @pemit or the buzzer @emit based on the condition. The first @switch that executes sees there's no attribute yet set for 'Whoever' and chooses the @emit branch. However, this branch is put at the end of the queue, so it isn't executed yet. Nothing has changed yet, so the second switch chooses the same branch. Again, nothing changed, the third branched also chooses the same branch.

So, a successful buzzer hit occurs three times. And on each one, the condition-changing data is set -- but too late.

Typical ways of mitigating this problem are elimination of conditional queueing (often achieved with side-effect functions) or by using semaphores. Neither solution is perfect.


See also: @break, Enough to Be Dangerous: Conceptual Models for MUSHcode (a detailed look at commands, functions, and queues in PennMUSH)