PSim is a process-oriented discrete-event simulation package based on threads. It is similar to C++Sim, but much simpler and thus much easier to learn and use (though of course having fewer features).
Contents of this page:
Note: No guarantees of any kind are made regarding the accuracy of this software or its documentation.
Go to Professor Matloff's simulation Web page.
First install the GNU pth threads library.
Next, make a directory for psim, say /usr/local/psim, creating subdirectories lib and include (again, if you choose some other directory for psim, change everything below accordingly), and do the following from whatever directory you've unpacked the PSim source in.
g++ -g -I. -I/usr/local/include -c PSimCode.C ar r libpsim.a PSimCode.o mv libpsim.a /usr/local/psim/lib cp PSim*.h /usr/local/psim/include
Also, you may need to have the following line in your .cshrc startup file:
setenv LD_LIBRARY_PATH /usr/local/lib
Be sure your application source file has a line
In accessing command-line arguments from within your code, remember that the first two arguments are the debug flag and the simulation time limit, followed by the application-specific arguments.
Don't forget to have your code set PTHSNAT, the number of application-specific threads.
To set up compiling, do the following:
set PthDir = /usr/local/ set PSimDir = /usr/local/psim
and then whenever you compile, say z.C, type
g++ -g -I$PSimDir/include -I$PthDir/include z.C\ -L$PSimDir/lib -L$PthDir/lib -lpsim -lpth -lm
This is a long line; the `\' allows one room to continue on a new line, in response to a `?' prompt. Note that the order of the command-line options here may be important, so stick to the order shown here.
Of course, various shortcuts to this can be set up in Makefiles, shell aliases and the like.
app_name debug max_simtime application_args
where "debug" is 1 for debugging, 0 otherwise.
The program simulates an M/M/1 queue. There is a single server, to which jobs arrive at random times, with the interarrival time having an exponential distribution. The service time is also random, with an exponential distribution. If the server is busy when a job arrives, the job joins a queue. (In this simulation, we are assuming that the server is some kind of machine, so we refer to it as the machine.)
We are interested in determining the long-run average wait time per job. There is an exact mathematical formula for this quantity, but here we will find the value via simulation instead, to illustrate PSim.
We compile the program and name the executable file mm1, and run it:
mm1 0 1000.0 1.0 0.5
Here we specify no debugging (0), a simulation time limit of 1000.0, a mean job interarrival time of 1.0, and a mean service time of 0.5. The output will be something like
mean queue wait for 594 jobs = 0.968920
In other words, up to simulated time 1000.0, the program simulated the arrival of 594 jobs, and their mean wait in the queue was 0.96.
We'll see below how this came about, by examining the source file MM1.C. It is assumed that you know C++, but you need not have any prior background with threaded programming or simulation.
First, a few words on threaded programming. PSim uses what is called process-oriented simulation, meaning that each different activity in the system being simulated is modeled by a "process," actually meaning a thread.
You will not be calling the pth thread library directly. Instead, you will make calls to PSim wrapper functions, which in turn call the pth functions. However, the key to understanding how to use PSim is understanding how the various threads relinquish control of the CPU, causing one thread to execute for a while, then another thread, then another, etc. The programmer must arrange things so that the correct sequence of thread alternation will occur. The following concepts may seem somewhat vague when you first read them, but there will be an example at the end, and will really become clearer when we apply them to PSim further below.
A thread is like a Unix process, and in fact is sometimes called a lightweight process. The difference is that although each thread has its own stack, thus its own local variables, the global variables are shared by all threads. (See my OS tutorial if you need review of or an introduction to OS process concepts.)
The pth thread manager acts like a "mini-OS." Just like a real OS maintains a table of processes, a thread system's thread manager maintains a table of threads. When one thread gives up the CPU, or has its turn pre-empted (see below), the thread manager looks in the table for another thread to activate. Whichever thread is activated will then resume execution at the line at which it had left off, i.e. the line at which it had relinquished control Ii classical computer science literature, we say that the threads are coroutines.
Thread systems are either kernel-level or user-level. In the former case, each thread really does act like a process, and in fact will show up on Unix systems when one runs the ps command. The different threads set up by a given application program take turns running, just like processes do. When a thread is activated, it is given a quantum of time, at the end of which a hardware interrupt from a timer cause control of the CPU to be transferred to the thread manager; we say that the thread has been pre-empted. This kind of thread system is particularly useful on multiprocessor systems, and is used in the widely-used Unix pthreads system (though not in all Unix threads systems) and also Windows NT threads.
User-level thread systems, on the other hand, are "private" to the application; running the ps command on a Unix system will show only the original application running, not all the threads it creates. Here the threads are not pre-empted; on the contrary, a given thread will continue to run until it voluntarily gives up control of the CPU, either by calling a yield function or by calling a function by which it requests a wait for some event to occur. Though user-level threads have the drawback that they cannot achieve true parallelism on a multiprocessor system, they allow one to produce much cleaner, clearer code. PSim uses user-level threads.
(By the way, Java threads can be of either type, giving the user a choice of user-level, called "green" in Java parlance, or what Java calls "native." The latter means that Java will use whatever type of threads is used in the underlying OS of the machine on top of which the Java Virtual Machine is being run.)
In pth, a thread gives up the CPU via a call to pth_yield(), or by a call to pth_cond_await(). In both cases control passes to the thread manager. In addition,, in the latter case there is also a notation made in the table which says that this thread is waiting for a certain condition to occur; until that condition occurs, this thread is not eligible for reactivation. (The condition, by the way, will take the form of a variable of type pth_cond_t; different threads may have different variables to wait for.) The condition will be triggered at some future time by some other thread, which will do so via a call to pth_cond_notify(); the pth thread manager then updates the waiting thread's entry in the thread table, indicating that it is now eligible for reactivation.
As a quick illustration in the pth context, suppose a program has three threads, named A, B and C. These will take the form of functions of type (void *) in our program.
Our program's main() function will actually be the first thread to run. In main() will be three calls to pth_spawn(), one for each of A, B and C. These cause the pth thread manager to add the three threads to the thread table, but they will not be run yet; remember, in user-level thread systems, a thread--in this case main()--retains control of the CPU until it voluntarily gives up control. Let us suppose that the latter occurs via a call to pth_await() which we have placed following the three calls to pth_spawn(); let's suppose we have named the condition variable for which it waits AllDone. The pth thread manager will then activate some other thread, and if our program is typical, we will have written the code in such a way that main() will never execute again until all the program's work is done and some other thread sets the AllDone condition.
When main() calls pth_await(), the pth thread manager will choose some other thread to activate. Though it is possible to set priorities among the threads, we will assume here that we have not done so. Thus the manager will essentially choose one of A, B and C at random, and activate that thread; let's say it happens to be B.
Again, remember that since pth uses user-level threads, B will now run until it voluntarily gives up the CPU, with a call to pth-yield() or pth_cond_await(). Suppose it does so with a call to pth_cond_await(), for condition variable BCond. That call causes the pth thread manager to run, and it will then choose either thread A or C to run next, again essentially at random. Say A is chosen, and that after A runs for a while, it calls pth_yield(). The manager will then run C (by default, as nothing else is ready to run). Suppose C calls pth_yield() after a while too; then A will run.
Now suppose that this time A calls pth_cond_notify() on BCond, and then calls pth_yield(). This time the pth thread manager will have a choice between running B or running C. The choice won't necessarily be B, but at least B now is eligible to run, and if we have done our coding carefully, eventually B will be run. (The word "carefully" here is key, though. If we are not careful, B may have a starvation problem. This may not occur with kernel-level threads, so user-level threads do require a bit more care.)
All of this will become clear as we analyze the code in MM1.C, so let's do so now.
Every PSim program has some application-specific threads. In our example here, we will have two such threads, one simulating the job arrivals and the other simulating the operation of the machine. (If we had been modeling a two-server system, we would have set up two server threads, and so on.) Every PSim program also has two built-in threads, main() and the event-list manager.
The event list is a linear linked list of pending events, ordered by time, with the earliest event at the head of the list. The event-list manager thread's action is to repeatedly loop around, handling one event per loop. At each iteration of the loop, the event-list manager does the following:
In our example here, our two application threads are given by the functions Arrivals() and Machine(). The PSim internal code (through use of the variable PTHSNAT) is set up in such a way that the event-list manager will never run unless neither Arrivals() nor Machine() is eligible to run.
To make the ideas more concrete, let's first set forth an example of a typical instance of the operation of the M/M/1 system
We start at time 0.0, and suppose the first arrival occurs at time 1.2. Arrivals() will create an instance of the class PTHSJob, including field swhich say that the event type is "arrival" and the event time will be 1.2, and then call PTHSHold() on that job. The latter function will act this job to the event queue, and then call pth_cond_await() to wait for the event to actually occur. (The condition variable is actually inside the class instance, but the PSim programmer doesn't see it.)
Recall that by calling pth_cond_await(), Arrivals() now gives up control of the CPU to the pth thread manager. What thread will it run next? It might consider main(), but as in our pth example above, main() is not eligible to run now, as it is waiting for an "simulation done" condition variable. How about Machine()? The thread manager may well select the Machine() thread to run, but that thread will also quickly give up the CPU, because it will execute code we've placed there for it to wait for a nonempty queue.
So, one way or other, the next thread to run with any substance will be the only one left, the event-list manager. It will delete the head of the list (which will now become empty again), advance current time to 1.2, and then call pth_cond_notify() on the condition variable Arrivals() had been waiting for, thus making Arrivals() eligible to run again. The event-list manager then calls pth_yield().
So, which thread will run now? The only one eligible is Arrivals(). To see what happens next, let's take a look at the relevant code in Arrivals():
NewArrival->PTHSHold(); NJobs++; MachineThread->PTHSAppAddToQAndWake(NewArrival);
Remember, Arrivals() had relinquished the CPU when it called PTHSHold(). Upon resuming execution now, Arrivals() will pick up right where it left off, i.e. on the line which increments NJobs. Now focus your attention on the line following, which calls PTHSAppAddToQAndWake on MachineThread. As the name of the function implies, this will add our newly-arrived job to the queue at the machine, and "wake" it. The latter refers to the fact that Machine() had been waiting for a condition variable related to the machine queue being nonzero. The "waking" consists of calling pth_cond_notify() for this condition variable,so that Machine() actually gets a chance to run when Arrivals() next gives up the CPU. . (It will be Machine(), rather than the event-list manager, which runs next. Recall that the PSim code is set up so that the event-list manager will always defer to Arrivals() and Machine().)
Machine() will now generate the service time, say 2.419, meaning that the "service complete" event will occur at time 1.2 + 2.419 = 2.619. To do this, Machine() again puts the event time and event name in the PTHSJob instance and calls PTHSHold(), as Arrivals() did earlier. That makes Machine() ineligible to run, but Arrivals() is still eligible, so it runs again.
Arrivals() will then generate the next interarrival time, say 0.72, meaning an arrival at 1.2 + 0.72 = 1.92. Again it will add this event to the event list and then wait. The event list now consists of two pending events, an arrival at 1.92 and a service completion at 2.619.
At this point, both Arrivals() and Machine() are waiting, thus ineligible to run. So, the event-list manager thread now runs. It deletes the head of the event list, advancing time to 1.92, and so on.
You may wish to rerun mm1, setting the debug flag to 1. The debugging output will tell you a lot about the event list and the occurrence of events. Here is what the first few lines might look like (of course, with different numbers from above, as we are dealing with random variables):
mm1 1 100 1 0.5 | more event queue at time 0.867277: arrival at 0.867277 server queue: job 0, arrival time 0.867277 event queue at time 1.679067: service at 1.679067 arrival at 1.804920 event queue at time 1.804920: arrival at 1.804920 server queue: job 1, arrival time 1.804920 event queue at time 2.264041: service at 2.264041 arrival at 2.590569
You now have a pretty good idea of how PSim works. To finish up, let's look at main():
In main(), we first call PTHSInit(). This initializes the PSim system-specific entities. Then the call to OurInit() does the same for the entities specific to this application. First OutInit() initializes the total queue wait to 0.0, and the queue length to 0.
In PSim, there is a C++ class named PTHSAppThread to represent application-specific threads such as Arrivals() and Machine(). In the special case in which the activity represents a server, there is also a special subclass derived from PTHSAppThread, named PTHSAppServerThread. In our function OurInit(), the actual establishment of Arrivals() and Machine() as threads is done by the constructor functions of the PTHSAppThread and PTHSAppServerThread classes; that need not concern us here, but it is necessary to know that the arguments are pointers to the corresponding thread functions. For example, in the line
ArrivalsThread = new PTHSAppThread(&Arrivals,NULL);
the first argument is the address of the function Arrivals(), defined earlier in this source file. What will happen is that a pth thread will be established for that function.
One can invoke new on PTHSAppThread multiple times, or in array form, producing multiple threads of the same type (i.e. running the same function). We might do this, for instance, if we had several machines instead of one in our example here. And we could use the second parameter in the constructor function to assign ID numbers to the multiple threads.
Note that OurInit() also sets PTHSNAT to 2. This variable is the number of application threads, i.e. the number of threads not counting main() or the event-list thread. H
Next, main() makes the call
This should be in every PSim main(). By this call, main() relinquishes control, allowing the application threads to run. Eventually both of these threads will finish their work, and main() will be the only thread left, and will be chosen to run by the pth thread manager. At that time, main() will resume right where it left off, and the line
printf("mean queue wait for %d jobs = %f\n",NJobs,TotWait/NJobs);
will be executed, and the program as a whole will exit.
The following are suitable for call from PSim application code:
Spawns an application thread.
Spawns an application server thread.
Used to advance simulation time. Adds the event to the event list, and then calls pth_cond_await() on this->PTHSEvntCond.
Wait for a condition associated with this application thread (internally, this->PTHSThreadCond).
Signal ("wake") the given application thread.
Insert this class instance into the event list.
Add this class instance to the server queue. If the queue had been empty, signal ("wake") this server thread.
If this server's queue is empty, call PTHSAppThread::PTHSWaitAppThread() on it, i.e. make it "sleep" until an application thread adds a job to the queue and "wakes" this server. But if the queue is not now empty, remove this job from the queue, and make it this server's current job.
Generates a random number from a uniform distribution on the interval (0,1).
Generates a random number from an exponential distribution having the given mean.
Debugging aid. Print the event list.
Debuggging aid. Print this server's queue.
Debugging aid. Prints the current status of all pth theads.
Wait for the simulation to end. Typically called by main().
Signal that the simulation is done.
Not suitable for call from application code, but described here for the purpose of document PSim internal operation:
Event-list manager. Here is the pseudocode.
while(1) while number of ready application threads < PTHSNAT ; delete head of event list advance simulated time to the time in the deleted element if simulated time > time limit call PTHSEndSm.PTHSSignalDone() wake the thread associated with the deleted element call pth_yield()
Derived class of PTHSAppThread, adding variables and functions specific to servers.
An individual simulation element, e.g. a job needing work at a machine. In many applications, this class is extended via C++'s inheritance capability, so as to add application-specific variables and functions.
Current simulated time.
Number of application threads. It is crucial that this be set correctly, as it is used by the event-list thread.
From the command line. A value of 1 triggers a call to PTHSEvntLst::PTHSPrintEvntList() at certain times, and you may wish to add your own debug code triggered by PTHSDebug = 1 as well.
As with any programs, PSim code should be debugged with the aid of a good debugging tool. See my debugging Web page.
When in, say, gdb, at the beginning you may get error messages like
Program received signal SIGUSR1, User defined signal 1.
You will get one such error for each thread. Just ignore them, and do a Continue operation in gdb.
Debugging simulation programs tends to be difficult, as with any program dealing with multiple concurrent activities. The best strategy to find a bug--and in fact, to verify the correctness of the program--is to step through the simulation with the debugging tool on a small problem, verifying that the different variables do have the correct values at the times you expect them to.
PSim has a couple of debugging aids of its own:
If the "debug" command-line argument is set to 1, the event list is printed out each time an event is about to be executed, and if the PTHSAppServerThread class is used, the queue is printed out each time it is added to. You can also call the functions PTHSEvntLst::PTHSPrintEvntList() and PTHSAppServerThread::PTHSPrintQ() directly.
Also, one can call PTHSThreadDump(), which is a wrapper around a pth call which will print out the pth status of all threads.