Dec 292017
Multi-threaded C++ library for Borland C++. | |||
---|---|---|---|
File Name | File Size | Zip Size | Zip Type |
$FILES.LST | 1212 | 349 | deflated |
$READ.ME | 911 | 478 | deflated |
BUFFERS.H | 4981 | 1453 | deflated |
EXAMPLE1.CPP | 1647 | 687 | deflated |
EXAMPLE1.EXE | 12716 | 7870 | deflated |
EXAMPLE2.CPP | 1953 | 784 | deflated |
EXAMPLE2.EXE | 12868 | 7958 | deflated |
EXAMPLE3.CPP | 1803 | 726 | deflated |
EXAMPLE3.EXE | 12798 | 7897 | deflated |
EXAMPLE4.CPP | 2800 | 976 | deflated |
EXAMPLE4.EXE | 13256 | 8158 | deflated |
MAKEFILE.MAK | 1730 | 666 | deflated |
THREADS.CPP | 28896 | 6989 | deflated |
THREADS.DOC | 26397 | 7776 | deflated |
THREADS.H | 5178 | 1693 | deflated |
THREADS.OBJ | 5982 | 3244 | deflated |
Download File MTL100JE.ZIP Here
Contents of the THREADS.DOC file
Page 1
Class DOSThread: a base class for multithreaded DOS programs.
---------------------------------------------------------------------
Author: John English ([email protected])
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
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
"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!
December 29, 2017
Add comments