One of my favorite uses of the COMMAND pattern is the ACTIVE OBJECT pattern.[1] This old technique for implementing multiple threads of control has been used, in one form or another, to provide a simple multitasking nucleus for thousands of industrial systems.
The idea is very simple. Consider Listings 21-2 and 21-3. An ActiveObjectEngine object maintains a linked list of Command objects. Users can add new commands to the engine, or they can call Run(). The Run() function simply goes through the linked list, executing and removing each command. Listing 21-2. ActiveObjectEngine.cs
Listing 21-3. Command.cs
This may not seem very impressive. But imagine what would happen if one of the Command objects in the linked list put itself back on the list. The list would never go empty, and the Run() function would never return. Consider the test case in Listing 21-4. This test case creates a SleepCommand, which among other things passes a delay of 1,000 ms to the constructor of the SleepCommand. The test case then puts the SleepCommand into the ActiveObjectEngine. After calling Run(), the test case expects that a certain number of milliseconds have elapsed. Listing 21-4. TestSleepCommand.cs
Let's look at this test case more closely. The constructor of the SleepCommand contains three arguments. The first is the delay time, in milliseconds. The second is the ActiveObjectEngine that the command will be running in. Finally, there is another command object called wakeup. The intent is that the SleepCommand will wait for the specified number of milliseconds and will then execute the wakeup command. Listing 21-5 shows the implementation of SleepCommand. On execution, SleepCommand checks whether it has been executed previously. If not, it records the start time. If the delay time has not passed, it puts itself back in the ActiveObjectEngine. If the delay time has passed, it puts the wakeup command into the ActiveObjectEngine. Listing 21-5. SleepCommand.cs
We can draw an analogy between this program and a multithreaded program that is waiting for an event. When a thread in a multithreaded program waits for an event, the thread usually invokes an operating system call that blocks the thread until the event has occurred. The program in Listing 21-5 does not block. Instead, if the event it is waiting for (elapsedTime.TotalMilliseconds < sleepTime) has not occurred, the thread simply puts itself back into the ActiveObjectEngine. Building multithreaded systems using variations of this technique has been, and will continue to be, a very common practice. Threads of this kind have been known as run-to-completion tasks (RTC); each Command instance runs to completion before the next Command instance can run. The name RTC implies that the Command instances do not block. The fact that the Command instances all run to completion gives RTC threads the interesting advantage that they all share the same runtime stack. Unlike the threads in a traditional multithreaded system, it is not necessary to define or allocate a separate runtime stack for each RTC thread. This can be a powerful advantage in memory-constrained systems with many threads. Continuing our example, Listing 21-6 shows a simple program that makes use of SleepCommand and exhibits multithreaded behavior. This program is called DelayedTyper. Listing 21-6. DelayedTyper.cs
Note that DelayedTyper implements Command. The Execute method simply prints a character that was passed at construction, checks the stop flag and, if not set, invokes DelayAndRepeat. The DelayAndRepeat constructs a SleepCommand, using the delay that was passed in at construction, and then inserts the SleepCommand into the ActiveObjectEngine. The behavior of this Command is easy to predict. In effect, it hangs in a loop, repeatedly typing a specified character and waiting for a specified delay. It exits the loop when the stop flag is set. The Main program of DelayedTyper starts several DelayedTyper instances going in the ActiveObjectEngine, each with its own character and delay, and then invokes a SleepCommand that will set the stop flag after a while. Running this program produces a simple string of 1s, 3s, 5s, and 7s. Running it again produces a similar but different string. Here are two typical runs: 135711311511371113151131715131113151731111351113711531111357... 135711131513171131511311713511131151731113151131711351113117... These strings are different because the CPU clock and the real-time clock aren't in perfect sync. This kind of nondeterministic behavior is the hallmark of multithreaded systems. Nondeterministic behavior is also the source of much woe, anguish, and pain. As anyone who's worked on embedded real-time systems knows, it's tough to debug nondeterministic behavior. |