home *** CD-ROM | disk | FTP | other *** search
-
-
- Page 1
-
- Class DOSThread: a base class for multithreaded DOS programs.
- ---------------------------------------------------------------------
-
- Author: John English (je@unix.brighton.ac.uk)
- Department of Computing
- University of Brighton
- Brighton BN2 4GJ, England.
-
- Copyright (c) J.English 1993.
-
- Permission is granted to use copy and distribute the
- information contained in this file provided that this
- copyright notice is retained intact and that any software
- or other document incorporating this file or parts thereof
- makes the source code for the library of which this file
- is a part freely available.
-
-
- 1. Introduction.
- ----------------
- Class DOSThread provides a framework for writing DOS applications
- which consist of multiple (pseudo-)parallel "threads" of execution.
- DOS was not designed as a multithreading operating system (in fact,
- it actively hinders multithreading by being non-reentrant) but this
- class allows you to create multithreaded DOS applications without
- worrying about such problems (although you should read section 11
- for some important caveats concerning direct calls to the BIOS).
-
- To create a thread using this class, you must derive a class from
- it which contains the code you want to be executed in a member
- function called "main". All you have to do then is declare an
- instance of your derived class and then call the member function
- "run" to start it running. Each thread will be executed in bursts
- of 1 clock tick (55ms) at a time before being suspended to allow
- the other threads a chance to execute. The length of the timeslice
- can be changed if necessary using the member function "timeslice".
- If required, timeslicing can be disabled entirely, in which case
- it is up to each thread to relinquish the use of the processor at
- regular intervals so that the other threads get a chance to run.
-
- Threads can delay themselves for a given number of clock ticks by
- using the member function "delay", they can relinquish the use of
- the processor to allow other threads to execute by using the member
- function "pause", and they can terminate themselves or each other
- by using the member function "terminate". It is also possible to
- inspect the current state of any thread (ready-to-run, terminated,
- delayed and so on) by using the member function "status" and to
- wait for a thread to terminate by using the member function "wait".
-
- Two additional classes (DOSMonitor and DOSMonitorQueue) allow you
- to derive monitor classes of your own to facilitate communication
- between threads. A monitor will normally contain data structures
- which can be accessed by several threads. You can guarantee that
- only one thread at a time is executing a monitor member function
- which accesses the data by calling the member function "lock" at
- the start of the monitor function. If any other thread is already
- executing a monitor function guarded by a call to "lock", the
- current thread will wait until it is safe to proceed. At the end
-
-
- Page 2
-
- of the monitor function, you should call "unlock" to allow any
- waiting threads to proceed. Monitors can also contain instances
- of DOSMonitorQueue which allow threads to suspend themselves in a
- monitor function until some condition has been fulfilled (e.g.
- that a buffer isn't empty). Some other thread executing within
- the monitor can resume any suspended threads when the condition
- is fulfilled (e.g. after a data item has been put into an empty
- buffer). A template class which implements a bounded buffer is
- included in this distribution. This is probably the commonest
- use of monitors in most applications, so it may well not be
- necessary to define any other monitor classes of your own.
-
- If you find this class useful or have any suggestions as to how it
- can be enhanced, please contact the author at one of the addresses
- given above. E-mail and postcards will both be welcome!
-
-
- 2. Deriving a new thread "MyThread" from class DOSThread.
- ---------------------------------------------------------
- Every thread is created by deriving a new class from the base class
- DOSThread. Each derived thread class must provide a definition of
- a member function called "main" which contains the code which the
- thread will execute. "Main" is declared like this:
-
- void MyThread::main ()
- {
- // code to be executed by your thread
- }
-
- The constructor for your derived class "MyThread" will invoke the
- constructor for DOSThread. The constructor for DOSThread requires
- a single unsigned integer parameter which specifies the size of the
- stack to be allocated for the thread. However, a default of 2048
- bytes is assumed, and if this is sufficient you need not explicitly
- call the DOSThread constructor at all.
-
- Having created a derived thread class, you can then declare instances
- of this class in your program, as for example:
-
- MyThread thread1; // a thread called "thread1"
- MyThread threads [5]; // five identical threads
-
- The threads you declare will not be executed until you call the member
- function "run", as follows:
-
- thread1.run ();
-
- "Run" returns a result to the calling program which is TRUE (1)
- if the thread was started successfully, and FALSE (0) if it could
- not be started (either because there was insufficient memory to
- create the necessary data structures or because it has already
- been started). Note that you cannot call "run" from your thread
- constructor since the virtual function "main" is not accessible
- until you have finished executing the constructor.
-
- Once a thread has been started successfully, it will be executed
- in parallel with the main program. The main program effectively
- becomes another thread (although it has no name, and it can only
-
-
- Page 3
-
- make use of the static functions "pause" and "delay" described
- below).
-
- The default is for each thread to be granted a "timeslice" of one
- clock tick (55ms). If a thread is still running when its timeslice
- expires, it is moved to the back of the queue of ready-to-run threads
- and execution of the next thread in the queue is then resumed. The
- static member function "timeslice" can be used to change the length
- of the timeslices used. "Timeslice" requires an unsigned integer
- parameter specifying the desired timeslice length in clock ticks,
- as for example:
-
- DOSThread::timeslice (18); // timeslice once a second (18 x 55ms)
-
- If the parameter is zero, timeslicing is disabled. In this case
- it is up to individual threads to relinquish control to each other
- by calling a member function which will cause another thread to be
- scheduled. A member function "pause" is provided for just this
- purpose, and is described below.
-
- "Timeslice" must be called before any threads are declared; as soon
- as the first thread has been declared, calls to "timeslice" will be
- ignored. This means you cannot dynamically change the length of the
- timeslice during execution of the program.
-
-
- 3. Writing the member function "main".
- --------------------------------------
- "MyThread::main" (the main function of your derived class) will be
- executed in parallel with the rest of the program once it has been
- started by calling "run" as described above. While "MyThread::main"
- can be written in exactly the same way as any other function, it is
- important to remember that it is sharing the processor with a number
- of other threads and that if it has nothing useful to do, it should
- allow some other thread to run. The member function "pause" lets you
- temporarily release the processor to another thread:
-
- pause (); // schedule another thread
-
- This is a static member function, so is can be called from any
- point in a program as "DOSThread::pause". Even if you are using
- timeslicing, it is a good idea to call "pause" if your thread is
- temporarily unable to proceed (e.g. it is waiting for a key to
- be pressed), as otherwise it will do nothing useful for several
- milliseconds until its timeslice expires and another thread gets
- a chance to run.
-
- You can also make your thread wait for a fixed time by using the
- static member function "delay", specifying the delay period as
- a number of 55ms clock ticks:
-
- delay (18); // delay for 1 second (18 x 55ms)
-
- Note that "pause" and "delay" are both static member functions
- which always affect the current thread. This means that you are
- not able to "pause" or "delay" any other thread. It also means
- that you can call these functions from the main program if you
- need to.
-
-
- Page 4
-
- When "MyThread::main" returns, the thread terminates. You can also
- terminate a thread explicitly using the member function "terminate".
- If another thread (or the main program) wants to terminate "thread1",
- it can do it like this:
-
- thread1.terminate ();
-
- This is potentially problematical, as you have no idea what "thread1"
- is doing at the time. A thread can also terminate itself:
-
- terminate ();
-
- which has the same effect as returning from the main function of
- the thread.
-
-
- 4. Initialisation and finalisation.
- -----------------------------------
- When a thread is declared by the main program or by another thread
- the constructor for class DOSThread is called to create the thread
- and any constructor defined by your derived thread class is then
- called to complete the initialisation. Note that a thread is not
- completely constructed until this sequence is complete, and in
- particular this means that you cannot call "run" from inside your
- derived class constructor to start the thread running immediately.
-
- When you reach the end of a block in which a thread was declared,
- the destructor for the thread will be called. Any destructor you
- provide in your derived class is called first (while the thread
- could still be running), and the standard DOSThread destructor is
- then called to wait for the thread to terminate before tidying up.
- This means that your destructor should not do anything which might
- cause the thread to fail. The member function "wait" allows you to
- wait for the thread to terminate, and your destructor should call
- this function before doing anything that might cause the thread to
- fail. In other words, your destructor should be written like this:
-
- MyThread::~MyThread ()
- {
- wait (); // wait for thread to terminate
- ... // do any class-specific tidying up
- }
-
-
- 5. Handling "control-break" and critical errors.
- ------------------------------------------------
- Class DOSThread provides a simple mechanism for dealing with events
- reported by DOS. The first such event is the "control-break" key
- being pressed to abort a program. Class DOSThread intercepts these
- events and sets an internal flag. Individual threads (or the main
- program) can call the static member function "userbreak" to test
- if control-break has been pressed:
-
- if (DOSThread::userbreak ()) ...
-
- The flag will remain set so that other threads can also inspect it.
- Alternatively, you can use the static function "cancelbreak", which
- is identical to "userbreak" except that it also resets the internal
-
-
- Page 5
-
- flag. This allows an individual thread to deal with a control-break
- event without any other threads being able to deal with the same event
- as well as providing a means for resetting the flag. If threads do
- not use either of these functions, control-breaks will be ignored
- completely.
-
- Critical errors (the familiar "Abort, Retry, Fail?" errors) can be
- generated by DOS if a disk is write protected or a printer is offline.
- Classes derived from DOSThread can provide a virtual function "error"
- to deal with any critical errors they may generate. Threads provide
- their own critical error handlers on an individual basis; the default
- handler just fails the operation. To provide a critical error handler
- for a thread class, define a member function "DOSerror" as follows:
-
- DOSThread::Error DOSerror (int N);
-
- The parameter N is the DOS code defining the cause of the error.
- "DOSerror" should return a result of "DOSThread::IGNORE" to ignore
- the error, "DOSThread::RETRY" to retry the operation that caused the
- error, or "DOSThread::FAIL" to fail the operation. Note that during
- critical-error handling, the only DOS services that you can use are
- functions 00 to 0C. Class DOSThread will intercept and ignore any
- other functions, as they would otherwise cause DOS to crash.
-
- The function "DOSerror" should never be called directly; it will be
- called automatically if an error occurs during execution of a thread.
-
-
- 6. Inspecting the status of threads.
- ------------------------------------
- The member function "status" allows you to determine what the
- status of a thread is at any time. It can be called as follows:
-
- state = thread1.status ();
-
- The result is a value of type DOSThread::State, which will be one
- of the following values:
-
- DOSThread::CREATED -- the thread is newly created and can
- be started by calling "run".
- DOSThread::READY -- the thread is ready to run (or is
- currently running).
- DOSThread::DELAYED -- the thread has delayed itself by
- calling "delay".
- DOSThread::WAITING -- the thread is waiting to enter a
- monitor function guarded by "lock".
- DOSThread::QUEUED -- the thread is inside a monitor and is
- suspended on a monitor queue.
- DOSThread::TERMINATED -- the thread has terminated.
-
-
- 7. Using monitors for interthread communication.
- ----------------------------------------------
- One of the problems with multithreaded programs is communicating
- between threads. Since you do not know when a thread will be
- rescheduled, it is unsafe to modify shared global variables as
- it is perfectly possible for you to be interrupted during the
- process of updating them. If another thread performs a similar
-
-
- Page 6
-
- update, you may well complete your update using out-of-date
- values when your thread resumes, which means that the global
- variables end up in an inconsistent and incorrect state.
-
- The base class DOSMonitor provides a basis for developing classes
- which allow safe interthread communication. All you have to do is
- to derive a class from DOSMonitor which encapsulates any data which
- will be updated by more than one thread and which provides access
- functions to access the data. Each access function should begin
- by calling the member function "lock" and end by calling "unlock".
- This will guarantee that only one thread at a time is executing an
- access function in any individual monitor. The general structure
- of a monitor access function is therefore as follows:
-
- void MyMonitor::access ( /* parameter list */ )
- {
- lock ();
- ... // access shared data as required
- unlock ();
- }
-
- Classes derived from DOSMonitor can also contain instances of
- class DOSMonitorQueue. Within an access function, you can call
- the member function "suspend" with a DOSMonitorQueue as its
- parameter to suspend the thread executing the access function
- until some condition is satisfied. This will allow other
- threads to execute access functions within that monitor. The
- other access functions can resume any threads suspended on a
- particular queue by calling the member function "resume"
- with the queue as a parameter. This will reawaken the threads
- suspended in that queue.
-
- Note that suspend should be called from within a loop; since
- "resume" will restart all the threads in the specified queue,
- it is not guaranteed that the condition for which the thread
- is waiting will still be true at the time the thread actually
- resumes execution. Thus to suspend a thread until a counter
- is non-zero, code such as the following should be used:
-
- while (counter != 0)
- suspend (some_queue);
-
- As an example, consider a monitor to provide a 20-character
- buffer to transfer data from one thread to another. It might
- look something like this:
-
- class Buffer : public DOSMonitor
- {
- char data[20]; // the buffer itself
- int count; // no. of chars in buffer
- int in; // where to put next char
- int out; // where to get next char from
- DOSMonitorQueue full;
- DOSMonitorQueue empty;
- public:
- Buffer () { count = in = out = 0; }
- void get (char& c); // get a char from the buffer
- void put (char& c); // put a char in the buffer
- };
-
-
- Page 7
-
- The class constructor initialises "count" to zero to indicate an
- empty buffer and sets "in" and "out" to point to the start of the
- buffer. Threads must then call "get" and "put" in order to access
- the contents of the buffer. Two DOSMonitorQueue instances are
- used; "full" is used to suspend threads which call "put" when the
- buffer is full, and "empty" is used to suspend threads which call
- "get" when the buffer is empty. The code for "get" would be like
- this:
-
- void Buffer::get (char& c)
- {
- //--- lock the monitor against re-entry
- lock ();
-
- //--- suspend until the buffer isn't empty
- while (count == 0)
- suspend (empty);
-
- //--- get next character from the buffer
- c = data [out++];
- out %= 20;
-
- //--- resume any threads waiting until buffer isn't full
- resume (full);
-
- //--- unlock the monitor to let other threads in
- unlock ();
- }
-
-
- 9. The class "BoundedBuffer".
- -----------------------------
- The class "BoundedBuffer" included in this distribution is a template
- class derived from DOSMonitor which implements a bounded buffer like
- the example above. You can create a 20-character buffer using this
- class as follows:
-
- BoundedBuffer<char> buffer(20);
-
- The type given in angle brackets <...> is the type of item that you
- want to store in the buffer, and the parameter value is the maximum
- number of items the buffer can hold. The following member functions
- are provided:
-
- get (item) -- Get the next item from the buffer and store
- it in "item". The function returns 1 (TRUE)
- if it is successful and 0 (FALSE) if the buffer
- has been closed (see below).
- put (item) -- Put a copy of "item" into the buffer. This
- function returns 1 (TRUE) if it is successful
- and 0 (FALSE) if the buffer has been closed
- (see below).
- items () -- Return the number of items in the buffer.
- close () -- Close the buffer to prevent further accesses.
- If you do not close buffers when you have
- finished using them, you run the risk of your
- program never terminating -- a thread may be
- suspended waiting for a character that will
- never arrive, which means that its destructor
- will wait forever for it to terminate.
-
-
- Page 8
-
- 10. Error handling in monitors.
- -------------------------------
- Monitors derived from class DOSMonitor should provide a virtual
- function called "error" which will be called if any errors are
- detected in a monitor. "Error" should be declared as follows:
-
- void error (DOSMonitor::ErrorCode);
-
- The parameter to "error" is a code for the error which has been
- detected. This can take any of the following values:
-
- DOSMonitor::NEW_FAIL -- there was insufficient memory
- to create the necessary data
- structures for the monitor.
- DOSMonitor::NO_THREAD -- a monitor has been called when
- there are no threads running.
- DOSMonitor::LOCK_FAIL -- the current thread is calling
- "lock" when it has already
- locked the monitor.
- DOSMonitor::UNLOCK_FAIL -- the current thread has called
- "unlock" without having locked
- the monitor.
- DOSMonitor::SUSPEND_FAIL -- the current thread has called
- "suspend" without having locked
- the monitor.
- DOSMonitor::RESUME_FAIL -- the current thread has called
- "resume" without having locked
- the monitor.
-
- The last five of these indicate a bug in the monitor code which
- should be corrected. The default action if a monitor does not
- provide a definition for "error" is to exit the program with an
- exit status in the range -1 to -6 (-1 for NEW_FAIL through to -6
- for RESUME_FAIL).
-
-
- 11. Potential problem areas.
- ----------------------------
- Class DOSTask uses an internal monitor to guard against re-entrant
- calls to DOS, as these are certain to crash your machine. Direct
- calls to BIOS functions are not protected in the same way. While
- BIOS calls are generally safer (they use the caller's stack), they
- still manipulate a global shared data area. It is therefore not
- advisable to call BIOS functions directly, as this can lead to
- hard-to-identify bugs resulting from an inconsistent internal
- state. However, C++ library functions normally use DOS services
- rather than calling BIOS functions, so most of the functions in
- the standard library are safe to use. The major exceptions to
- this are the functions defined in <bios.h> and the functions
- "int86" and "int86x".
-
- If you do need to use BIOS functions directly, the best approach
- to adopt is to localise all BIOS calls in a single monitor so that
- only one task at a time can call a BIOS function; however, since
- DOS services will perform their functions by making BIOS calls,
- you must also use the monitor to encapsulate all DOS calls to
- guarantee that only one task at a time is making a BIOS call.
- This may not be a terribly practical solution.
-
-
- Page 9
-
- Another point worth noting is that screen output is best done
- using "fputs" rather than "cout", "printf" or "puts". Each of
- these generates several DOS calls to generate their output, and
- it is therefore possible for another thread to interleave some
- other output with it. In particular, if you use "cout" it is
- possible for the same output to appear twice if the thread is
- interrupted after the output has been displayed but before the
- internal buffer has been cleared. The next thread which uses
- "cout" will have its output appended to the existing contents
- of the buffer which will then be displayed in its entirety.
-
- A more serious problem (which I have been completely unable to
- resolve) is that programs which use direct BIOS calls can crash
- the system if high memory is being used. If your program needs
- to use direct BIOS calls, you should only do this if you are
- NOT using an upper memory manager such as EMM386 or QEMM. There
- is obviously some memory management context information which
- needs to be saved on a thread context switch, but without any
- knowledge of the internal workings of upper memory managers I
- do not know how to proceed on this (and if anyone can help me
- here, I will be eternally grateful!).
-
-
- 12. A plea for feedback.
- ------------------------
- If you use this class, please contact the author via the addresses
- at the beginning; if you don't have e-mail access please send me a
- postcard (I like postcards!) just to let me know you've looked at
- it. Feel free to suggest enhancements, find bugs or (better still)
- fix them and send me patches. Happy hacking!
-