Download Preparing your Paper for MSC 2000

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts
no text concepts found
Transcript
DISTRIBUTED AND CONCURRENT TEST ENVIRONMENTS ON
POPULAR COTS PLATFORMS
Raymond J. Toal, Ph.D.
Loyola Marymount University
7900 Loyola Blvd
Los Angeles, CA 90045-8130
310.330.2773
[email protected]
Abstract
Today's
automatic
test
system
implementations likely require some aspect of real-time
performance, concurrency, and network connectivity.
Users expect responsive, efficient operation, both at the
local operator interface as well as remotely via a LAN or
the Internet. Distributed, concurrent COTS software
systems, such as Sun Microsystem's Java and
Microsoft's Win32 are a natural fit to these
requirements.
Based on real-world experience developing distributed,
concurrent test systems, this paper reviews some of the
unexpected obstacles implementers might encounter.
We discuss the creation of threads, thread scheduling,
and the use of thread synchronization constructs. We
compare these mechanisms to those available in IEEE
Standard ATLAS and test-specific programming
frameworks.
INTRODUCTION
The economics of today's Automatic Test System (ATS)
business demands a Commercial, Off The Shelf (COTS)
solution whenever possible, including the software
systems. At the same time, ATS users expect reliability
beyond that offered by typical commercial software. And
due to the popularity of desktop operating systems and
applications, every ATS user is an expert on user interface
responsiveness and functionality. The era of the dumb
terminal connected to a minicomputer is rapidly fading
into distant memory. New ATS designs are more likely
than ever to be PC-based, implement a graphical user
interface (GUI), and be connected to a local network or
the Internet. Many of these systems have no particular
test language requirement, especially smaller systems.
Robert G. Hayes
Raytheon Infrared Operations
75 Coramar Drive
Goleta, CA 93117-3090
805.562.4226
[email protected]
Two popular programming frameworks that meet these
requirements are Sun Microsystem's Java and Microsoft's
Win32 API. Both support multi-threading, a powerful
mechanism for efficient implementation of concurrent and
distributed systems. However, developers educating
themselves in the art of multi-threaded programming on
these platforms must fight their way through several
confusing and counterintuitive issues. Software designers
must understand not only the theoretical aspects of
concurrency, but a stunning amount of peculiar
implementation details as well before they can implement
robust systems with any degree of confidence.
After a brief overview of concurrency features in IEEE
Standard ATLAS, we discuss several of the potentially
problematic issues in our example COTS platforms.
These include the creation of threads in Win32, thread
scheduling under Java and Win32, and the use of monitors
and pipes in Java. We also show the importance of
distinguishing threads and thread objects, concepts
sometimes confused in object-oriented environments. We
finish with a review of the new real time specification for
Java.
BACKGROUND
Multi-tasking provides for multiple processes to
simultaneously exist and execute on a system. Truly
concurrent process execution occurs only on multiprocessor systems; pseudo-concurrency is achieved on
mono-processor systems via various scheduling schemes
that allocate processor time amongst tasks. Multi-tasking
can be achieved by allowing multiple processes to exist
simultaneously, with execution interleaved by preemption
or time slicing techniques. However, a finer granularity of
task execution is possible through the concept of threads.
A thread is a basic unit of processor utilization, consisting
of a program counter, registers, and stack [1]. A thread
and its peer threads within a given task share code, data,
and operating system resources. A task must have at least
one thread - a traditional process is a task with exactly one
thread. A multi-threaded operating system such as
Microsoft Windows, in its various flavors, or Sun's Java
Virtual Machine, schedules processor execution on a perthread basis. When execution is switched from one thread
to another within a multi-threaded task, a context switch is
said to occur. Since context switches between the "light"
threads are cheaper computationally than switching among
"heavy" processes, overall system operation may be more
efficient. The degree of efficiency gained depends upon
the programmer's ability to structure the system into multithreaded tasks.
An ATS can benefit from multi-threading in many ways.
One obvious example is the use of a Graphical User
Interface (GUI). One or more threads can be created and
dedicated to handling responses to user actions such as
mouse clicks, while using other threads to perform
instrument control, print requests, data logging, etc. By
prioritizing the threads, the system designer can ensure
timely response to user inputs, while at the same time
managing the system for overall efficiency. Most any test
operator appreciates an immediate response to an ABORT
button mouse click without waiting for the system to
discover it buried in some obscure event queue. A high
priority thread dedicated to monitoring an abort button is
one solution, though this may not be as simple as it seems
as we will discuss later.
Functional testing of Units Under Test (UUT's) that
contain embedded processors often requires some manner
of multi-tasking. A multi-threaded test program can be
tailored to achieve complex stimulus and response
functionality in a straightforward manner. Stimulus or
response operations can be allocated to threads dedicated
to each operation. Groups of similar “worker” threads can
be created and maintained in a pool, and assigned in
response to system events. Access to shared data
structures can be coordinated by the operating system,
maintaining integrity and ensuring deterministic operation.
Thread priorities can be assigned to achieve required
response times to external events.
This isn't to say that a multi-threading approach should be
used without good reason - it's all too easy to create a
multi-threaded nightmare if the solution isn't well thought
out. And alternative solutions are available on many
operating systems that may be more easily achieved.
CONCURRENCY AND IEEE ATLAS
IEEE ATLAS implementations typically offer only
rudimentary provisions for concurrency, if any. The "nonliving" IEEE Standard 416-1984 ATLAS defines a TASK
structure, along with a SEMAPHORE type and
SUSPEND
and
CONTINUE
verbs
for task
synchronization. A TASK has a priority, and its own state
in the form of the MEASUREMENT variable and the
built-in ATLAS condition flags. A defined TASK is
instantiated with the ENABLE TASK statement, and can
be designated as continuous or repetitive for some number
of iterations. A TASK can be associated with an event
via the IDENTIFY EVENT statement, allowing the actual
execution of an enabled task to be synchronized with an
event. A task can be terminated using the DISABLE
TASK statement, but with semantics that vary depending
upon whether the referenced TASK has begun execution.
If the TASK is enabled but awaiting an EVENT it is
terminated immediately.
A TASK that has begun
execution is allowed to complete its current (and possibly
only) iteration and is then disabled. This allows the
TASK to place the UUT into a known state and clean up
in an orderly manner, avoiding non-deterministic
behavior. Protected regions of code can be designated
with the REQUEST and RELEASE statements. Transfer
of information between tasks is limited to the use of
global variables.
IEEE Standard 416 ATLAS also provides the ability to
delimit groups of statements that have particular execution
requirements.
The PREPARE, SEQUENTIAL
OPERATION
and
PREPARE,
CONCURRENT
OPERATION statements, coupled with the EXECUTE
statement, designate a group of statements that must be
executed within a critical timing constraint.
IEEE Standard 716 ATLAS provides only the DO
SIMULTANEOUS construct for achieving a form of
concurrent execution.
Within the body of a DO
SIMULTANEOUS construct, only ATLAS statements
that contain a critical action are allowed. A critical action
is that specific point in time within the execution of a
multiple-action signal oriented ATLAS verb where the
ATLAS statement is considered to have achieved its
purpose. The specific instant is defined in terms of state
transitions within virtual resource state diagrams that are
part of the standard. The concept here is that the instant
of critical action for each statement within the body is
achieved simultaneously. No provision is made for data
dependencies that may be inherent among the multiple
statements within a given DO SIMULTANEOUS
construct. No data processing or non-signal oriented
operation can occur - critical actions are not defined for
these classes of statements and consequently they cannot
be included.
IEEE Standard 416 ATLAS provides a multi-tasking
capability more in line with the concept of multi-tasking
as is provided by current popular operating systems. It
also provides the ability to denote blocks of statements as
having critical execution timing constraints.
IEEE
Standard 716 ATLAS provides a capability oriented
towards achieving a very precise, signal oriented
concurrency that most likely is actually implemented by
trigger and timing circuitry within and between real
instruments, rather than a thread execution concurrency or
pseudo-concurrency implemented by an operating system.
A more comprehensive analysis of multi-tasking issues
and ATLAS has appeared in these proceedings
previously[2].
WIN32 THREADS
When first learning about threads in Win32, the
CreateThread function is encountered. The function is
straightforward and readily understood.
If the developer happens to be a Microsoft Visual C++
programmer, however, he next learns that he shouldn’t be
using the CreateThread function at all, as it doesn’t
initialize the C runtime library! Among other problems,
static data structures within the library (e.g. errno) end up
shared between the various threads, making it likely one
thread will get incorrect data from the library. Upon
reflection, it seems perfectly reasonable that
CreateThread is language-neutral, and cannot be
expected
to
perform
language-specific
library
initialization.
The developer next learns about
beginthread, a Microsoft C library call. Yet another
disappointment: this function has a built-in race
condition [3].
He really should be using
_beginthreadex.
But first, he must determine which of the six Microsoft
Visual C++ runtime libraries to use. And once that is
accomplished, the next task is to figure out how to get the
_beginthreadex call to compile, as Microsoft changed
the parameter types from those of CreateThread to nonWin32 types, in an unsuccessful1 attempt to achieve
operating system neutrality. Casting away the type
differences or weakening the compiler error checking
helps. But didn't Bjarne Stroustrup say something about
most casts being an indication of a design error [4]?
1
The programmer still has to close the thread's (Win32)
handle.
Furthermore, the calling convention for the thread
function
differs
between
CreateThread
and
_beginthreadex, making porting to _beginthreadex a
treacherous task.
As it turns out, some C runtime libraries are inherently
multi-thread safe, without the need for any special thread
creation call.
National Instruments, with its
LabWindows/CVI integrated development environment,
provides such a C runtime library. The library creates a
thread-specific copy of the data structures in thread local
storage as needed, on the fly. Single-threaded users
access errno as usual, and multi-threaded users access a
thread-specific copy of errno with a function call,
implemented by an errno macro.
Even Microsoft’s C runtime libraries can pull off the same
trick (although with a potential memory leak) [5]. A
multi-threaded application that uses CreateThread (and
fails to explicitly initialize the C runtime library) may
work after all, even though it calls the C runtime library
from secondary threads.
This is a confusing state of affairs. And we haven't
mentioned AfxBeginThread, CreateRemoteThread, or
CWinThread::CreateThread, three more ways to start a
thread in Win32. Furthermore, there are two overloaded
versions of AfxBeginThread (one for user interface
threads and one for worker threads), and two different
MFC class libraries containing them both.
Having mastered the creation of multiple threads within
his application, the developer faces the challenge of
coordinating them. Once he figures out that C/C++
signals don't work under Win32, the thread
synchronization mechanisms that are provided —
semaphores, events, critical sections, mutual exclusion
and so on — are relatively straightforward. Coordination
of both thread execution and shared data access is easily
achieved.
Win32 threads are preemptively scheduled according to
priority. A thread's base priority is determined by adding
the value of its process's priority to a relative thread
priority. The resulting priority is a value from 0 through
31, lowest to highest. A queue exists at each priority level
for threads ready to run. The threads in the highest level
non-empty queue are executed in round robin fashion. A
priority boost mechanism dynamically adjusts thread
priorities for a variety of reasons. More deterministic
thread scheduling can be obtained in Windows 2000/NT
by using an API call to turn off the dynamic priority
boost. Implementers should note that Microsoft has
implemented slightly different thread scheduling schemes
on the various Windows platforms, and on some the
scheme even varies with the particular version of the
platform.
Under Windows 95 and Windows 98, there’s no way to
prevent any Win32 thread from arbitrarily boosting its
own priority, both relative to the process and that of the
process itself. The Windows 2000/NT operating systems
provide a security mechanism that treats threads and
processes as securable items, allowing the software
designer to associate thread priorities with user privileges.
It's not possible in Win32 to throw an exception from one
thread into another thread. There's a good reason; if the
thread receiving the exception were in the middle of a
critical section, non-deterministic operation could result.
But everyone at one time or another (after ensuring the
safety of the operation) wants to do it, and you can’t do it
at all in Win32. If one thread needs to interrupt another, it
can only do so if, by design, the “interrupted” thread
periodically checks or waits upon a flag. This smacks of
polling, and clutters the code since threads should
normally be free of such mundane scheduling concerns.
Java at least offers the capability with the interrupt
method (along with dire warnings of when not to use it
and severe restrictions on when the interrupt will actually
occur). The IEEE Standard 416 ATLAS language
provides the ESCAPE mechanism for the same purpose,
whereby one task can signal another task to interrupt its
normal flow of execution and switch to something else
(e.g. diagnostic code).
Finally, the developer faces a complex set of Win32
thread termination functions, each with slightly different
semantics. Proper termination of multiple threads is
almost as difficult as getting them started. Thread
creation functions are paired with specific thread
termination functions, and these must not be mismatched.
Microsoft C and C++ programmers have another
implementation detail to worry about: the C runtime
library startup code calls ExitProcess shortly after
returning from main. This means any worker threads will
not be allowed to outlive the main thread; the programmer
must explicitly tell the main thread to wait for the workers
to complete. This is an absolute surprise to Win32
experts who know a process normally lives as long as at
least one thread (any thread) is running, as well as to Java
programmers who are also not used to treating the main
thread in a special way.2
2 Attempting to allow worker threads to outlive the main
thread by ending main with a call to ExitThread in place
of return is poor practice since it bypasses cleanup of the
C runtime library.
Win32 supports the various Component Object Model
(COM) threading models. The usefulness of COM and
ActiveX in ATS has previously been discussed [6]. But
understanding the COM threading models - single,
apartment, free, and both - can be a daunting task. A
sound understanding of Win32, C++, and COM is
required. Inefficiencies and unexpected behavior can
result when Win32 threads are improperly combined with
the COM threading models [7].
Windows NT also supports the concept of fibers. The
Windows NT operating system code that manages threads
is in the kernel. A thread context switch requires switching
between the user mode of the application code and the
kernel mode of the thread manager, which incurs a
performance penalty. Fibers are a subcomponent of
threads that are managed by code running in user mode.
Scheduling of a fiber is achieved by switching to it from
another fiber within the application using the
SwitchToFiber Win32 API call.
Fibers are not
preemptively scheduled by the operating system, and
therefore no thread-scheduling overhead is incurred.
Multiple fibers can exist in a thread, and they all execute
within the context of the thread that created them.
Microsoft introduced fibers primarily to facilitate porting
Unix server code to Windows NT, and they are otherwise
rarely used, as they offer no advantages over a properly
designed, multi-threaded application.
Win32 offers alternatives to multi-threading that may be
both more efficient and simpler conceptually, depending
upon the application. Win32 offers asynchronous I/O, I/O
completion ports, asynchronous procedure calls, and the
ability to wait for multiple events. As an example, one
thread waiting on multiple events may be more efficient
than a scheme using multiple threads, each waiting for a
single event. And even a single thread waiting for an
event avoids polling or spinlocks, which consume
processor cycles needlessly.
NATIONAL INSTRUMENTS
LABWINDOWS/CVI AND WIN32
National Instrument's (NI) LabWindows/CVI supports
access to the Win32 API, including the threading
functions. But prior to the most recent implementation,
Version 5.5, the Lab Windows/CVI Integrated
Development Environment (IDE) didn't support the
debugging of a multi-threaded application, even though it
was possible to create one. And not all of the earlier Lab
Windows/CVI libraries were multi-thread safe. Even with
a "multi-thread safe" library, peculiarities might only be
discovered after many hours running down anomalous
multi-threading behavior.
Here's an example. We recently implemented a multithreaded Win32 application using LabWindows/CVI and
the NI "multi-thread safe" data acquisition library, which
is implemented as a Dynamic Link Library (DLL). The
same DLL is used for many of NI's analog input and
output products. The target system required an analog
output signal be programmed to 10VDC in the event of a
system abort, providing a system fail signal to other nodes
in the system. A high priority thread was used to
implement the abort: it performed some cleanup and then
attempted to program a NI analog output card to 10 VDC.
Unfortunately, since the main thread might be
coincidentally using the same DLL to acquire data from a
separate, analog input card, it could be within the DLL in
a protected region, blocking the high priority abort thread
and causing a deadlock condition. Since the protected
code within the DLL was relatively small, with the abort
thread only very rarely being blocked, this error went
undetected for some time. Because Win32 doesn't allow
throwing an exception from one thread into another, nor
are C signals supported, we tried simply terminating the
main thread from the abort thread, to free up the DLL.
We weren't concerned about failing to clean up the C run
time library, as the abort thread was designed to terminate
the process and shut down the operating system once the
fail signal was generated. The Win32 documentation
states that when a thread is terminated, all system objects
(such as mutexes) that the thread has referenced become
signaled, and are released if there are no other references.
Perhaps the DLL was using a critical section rather than a
system object to protect the region. In any event, the DLL
didn't clean up enough to allow the abort thread to
continue, and we were forced to abandon the concept.
On the positive side, the Lab Windows/CVI 5.5 IDE
implements a full multi-thread debugging environment
that's a big improvement over previous versions. And NI
claims that all of the libraries are multi-thread safe.
Caution must still be exercised to avoid problems such as
the non-obvious deadlock condition described above. A
Microsoft Platform Developer's Kit is bundled into the
IDE that provides ready access to all things Win32,
including Windows 2000, but not Windows CE.
The LabWindows/CVI libraries have overloaded several
of the Win32 API calls, which can cause no end of
confusion. For example the utility library function
GetSystemTime, supported on Microsoft Windows
platforms only, inexplicably overloads the well-known
Win32 call of the same name. We were forced to edit the
Lab Windows/CVI utility library header file in order to
access the Win32 function.
JAVA THREADS
With Lab Windows/CVI 5.5, NI introduced a set of utility
library functions intended to facilitate the use of threads.
Various functions include the ability to create and discard
a thread pool, a thread-safe queue, a thread-safe variable
(NI cautions that this function is "difficult to use"), a
thread lock, and a thread local variable. It seems to us
that the developer may well be better off dealing with the
Win32 calls directly, rather than using these proprietary
calls which are unique to NI. This is especially true if one
has some basic understanding of Win32 threading issues
to start with.
VISUAL BASIC AND MULTITHREADING
Microsoft's Visual Basic has achieved some measure of
popularity as an ATS programming and implementation
language. And Visual Basic supports Win32 API calls,
including those used to create and manipulate threads,
such as CreateThread. By some reports, this has
resulted in substantial confusion. COM, which forms the
basis for Visual Basic objects, implies a threading model
as part of the COM contract. It's possible to violate the
COM object's threading contract by calling methods
within the object from multiple threads, when the object
expects all calls to come from the same thread. At least
one Visual Basic expert recommends that implementers
not use the Win32 CreateThread call at all [8], and use
only the multi-threading techniques provided in
Microsoft's Visual Basic documentation. This is perhaps
not so much a commentary on Visual Basic or Win32, but
rather on the propensity of Visual Basic programmers to
get into trouble with COM objects when implementing
multiple threads. However, prior to Visual Basic 6.0,
various parts of the Visual Basic runtime engine were
apparently not multi-thread safe.
Java continues to gain popularity as an object oriented
language that has been designed to avoid many of the
pitfalls programmers encounter with C++. It's purported
ability to run the same code on multiple platforms would
seem to offer advantages to ATS implementers, especially
if the application is distributed across multiple,
heterogeneous systems. As we shall see, however, Java
has some pitfalls of its own when it comes to multithreading.
Java is an object oriented language, yet threads are a
procedural mechanism.
Threads are sequences of
execution. Even in a well-designed object oriented
program, it may be very difficult to follow the thread of
execution from object to object. Often object-oriented
programmers have to fight the urge to identify the thread
with the thread's (or runnable object's) run method. This
confusion is magnified when we diagram threads in
modeling languages that emphasize object-orientation,
such as the Unified Modeling Language (UML). In a
typical class or object diagram, a thread class or object is
rendered in a rectangle with a thick border, but the
behavior of the thread requires a state or interaction
diagram. At any time, the locus of control for a thread is
often not in the run method of the thread, but rather in a
method of some other object. The concept of program
execution is typically abstracted into messages amongst
collaborating objects; thus the programmer requires a
mental effort to distinguish the collaboration from the
"active" object that initiated the thread.
Java claims to implement a simple, deterministic,
preemptive, priority-based thread scheduler [9]. The fact
of the matter is that the Java runtime specification does
not require any particular thread scheduling mechanism to
be implemented within a Java execution environment.
Java speaks to the issue of the threading model in terms of
green threads and native threads.
In a green threads implementation, the Java execution
environment does all of the thread scheduling, and it tries
to ensure that the threads are scheduled in a deterministic,
preemptive manner according to priority. What actually
happens is that at any given time, a thread with higher
relative priority than another is more likely to be running.
Green threads are more portable than native threads, but
their behavior can vary among Java execution
environments.
For example, some green thread
implementations may perform round robin scheduling of
same-priority threads, and some may not. Green threads
are similar to the concept of fibers or user-threads. Green
threads perform cooperative multi-threading unbeknown
to the underlying operating system. A green thread is not
mapped onto a native (operating system) thread; all the
green threads are probably lumped into a single native
thread that is in turn interleaved with the other native
threads.
for Win32 Java execution environments is that while there
are ten priority levels defined for the Java Thread class,
there are only seven thread priorities within a given
Win32 scheduling class (i.e. process priority). So while
the Java threads are each mapped onto a native Win32
thread, the relationship among the thread priorities may
lose fidelity with the original.
Sun’s advice is to not rely absolutely on the
“deterministic” threading model; thread priorities offer
some control over thread scheduling, but are not a
guarantee of deterministic scheduling behavior. For
example, Sun cautions against relying on mutual exclusion
achieved via thread priorities.3
Java developers are faced with something of a dilemma.
The more portable green threads probably run
cooperatively and might not be able to take advantage of
operating system aids such as time slicing and thread
assignment to one of multiple processors for truly
concurrent execution. On the other hand, native threads
can vary quite a bit from implementation to
implementation. Java’s “write once run everywhere”
mantra is ill served by the variance among platforms.
Another issue affecting thread scheduling determinance in
Java is garbage collection. A Java Virtual Machine
(JVM) typically implements a daemon thread that runs
from time to time whenever other threads exist and frees
objects that are no longer referenced.
For many
applications this is a benefit - the programmer does not
need to be concerned with memory management and the
destruction of unused objects. For ATS implementers,
however, the inability to control garbage collection
scheduling can be an obstacle to achieving temporal
determinism.
The native threads model allows the Java threads to be
scheduled by the underlying operating system, since the
Java threads are mapped onto operating system threads.
And since all operating systems tend to differ in their
scheduling algorithms, so too will the behavior of a given
Java application. Even the various Win32 platforms vary
significantly with regard to thread scheduling.
The Sun tutorial on Java threads [9] includes a Dining
Philosopher’s
applet
to
demonstrate
thread
synchronization and scheduling. We were amused to see
that it occasionally fails on Windows 98 running on a 350
MHz Pentium II system using Netscape Navigator 4.6.
Sometimes the GUI thread gets out of synch with the rest
of the applet, and one of the philosophers (actually,
they’re all instances of Duke, Sun's coffee-drop Java
mascot) has both hands down (hasn’t picked up a
chopstick) when deadlock occurs.
The operating system may or may not perform dynamic
priority boosts for various reasons, it may or may not
perform round robin scheduling, it may or may not
support true concurrency on multiple processors, and it
may or may not detect and try to prevent priority
inversion. All of these issues affect the performance of
the Java application. Another commonly cited problem
3 A programming ploy from decades past on operating
systems that didn't provide mutual exclusion natively.
MONITORS IN JAVA
Every object in a Java program is a monitor, i.e. an object
that can coordinate access to its state among multiple
threads and even allow these threads to synchronize on
arbitrary conditions via wait and notify methods. The
originator of the monitor concept was Tony Hoare, and
his original definition [10] is what most students first
encounter. Java doesn't implement the monitor as defined
by Hoare, however. Java more nearly implements what is
known as the Lampson/Redell monitor [11].
You may have noticed that any proper description of
Java’s wait method depicts its use within an enclosing
while statement that rechecks the condition that caused
the wait. Upon awakening from the wait, the thread inside
the synchronized method must verify that the condition
that caused the original wait has not re-occurred. This is
necessary because Java uses a single lock to implement
both the lock for entering the monitor, and that for waiting
on an event "inside the monitor." The awakening thread
cannot be guaranteed to have acquired the lock: a "lucky"
thread may enter at just the right moment and acquire the
lock before the awakening thread. Unless the awakening
thread re-checks the condition, ambiguity can result.
In a Hoare monitor, threads that have entered the monitor
and are waiting on a conditional variable have precedence
over threads that are queued outside the monitor waiting
to get in. That is, there are two queues associated with the
monitor: one for threads awaiting entry into the monitor,
and another for the condition inside the monitor where
threads are waiting for a signal. Upon receiving a signal,
one thread from the condition queue is selected to run.
The Hoare monitor does not specify any particular method
of picking a thread to run from the condition queue.
Typical implementations are first come first served or
priority. There are several variations of the Hoare
monitor, mostly concerned with the scheduling of the
signaling thread vs. a waiting thread.
One of the arguments in favor of the Lampson/Redell
monitor is that it does not require perfectly reliable thread
scheduling (as does a Hoare monitor). It seems to us,
however, that the original Hoare monitor is the more
intuitive.
Protected objects in Ada [12] have the
semantics of the Hoare monitor. Java’s designers may
have chosen to implement the Lampson/Redell monitor in
order to decouple Java monitors from a dependence on
thread scheduling, which may vary from implementation
to implementation.
Java apologists claim that the original Hoare monitor can
be constructed from the provided mechanism, and that it’s
all just an issue of what one is used to. But if you decide
to synthesize your own monitor construct, watch out for
the dreaded "nested monitor deadlock" [13].
In what must be the ultimate word on the matter,
Disneyland recently implemented an "express line"
concept for their more popular attractions that is very
nearly an example of a Hoare monitor. Rather than wait
in line, patrons can elect to be given a time to return
(typically about an hour later) when they will enter an
express line which has priority over the regular line. If it
were a Java monitor, a returning "express" patron would
have to race the person at the front of the regular line for a
seat on the ride. If he lost, he'd have to return to the
express line, and keep racing the person at the head of the
regular line until he found someone he could beat.
A description of Hoare and Lampson/Redell monitors and
their relative merits can be found in [14].
JAVA PIPES
Java pipes are often represented as being “like Unix
pipes” but they’re not. Unix pipes are an inter-process
communication (IPC) mechanism, and Java pipes are an
inter-thread mechanism. Unix pipes are self-serialized:
when two or more processes simultaneously attempt to
write to a pipe, one and only one process succeeds, and
the data from the successful process is sent to the pipe. In
Java, this doesn’t appear to be true. If two threads
simultaneously attempt to write into a pipe the data in the
pipe may be interleaved. Users must serialize the pipe
data for themselves with a synchronization object that
writes to the pipe on their behalf.
Another issue with Java pipes is that Java provides no
mechanism similar to Win32 handles, whereby the user is
required to notify the OS that it will be making a reference
to an object, such as a pipe. In Win32, prior to using a
pipe, a user must obtain a handle to it. In doing so, the
system increments a reference count of the processes
using the pipe. If a process holding a handle to a pipe
dies, the system decrements the reference count. If the
reference count is then greater than zero, the system
knows that at least one other process has a handle to the
pipe, and the pipe stays in existence.
Java can’t do this. The Java execution environment has
only ad-hoc awareness of what threads are connected to
the pipe. When a thread writes into a pipe, for example,
the execution environment logs the thread as a pipe-writer.
If that thread subsequently dies, the execution
environment takes note. The next time a thread attempts
to read the pipe, the execution environment throws a
“write end dead” exception. There may well be another
thread that intends to write into the pipe at some point, but
if it has not as yet done so, the execution environment is
not aware of the fact. Worse still, depending upon the
processor speed, the threading model, the distribution of
thread termination times, and the underlying OS, the
execution environment may exhibit non-deterministic
behavior. The program may appear to operate correctly
when in fact it is erroneous.
A developer can avoid many problems with Java pipes by
ensuring that a pipe have only one reader and one writer,
and ensuring that the writer does not die until after the
reader has read the final message sent by the writer. But if
the Java designers intended a pipe to have only a single
writer, why didn't they at least require registration of a
writer with a pipe?
REAL-TIME JAVA
The attractiveness of a relatively safe, object oriented
language such as Java for use in ATS is diminished by
many of the problems we have cited. Realizing that Java
might be extended to accommodate applications with realtime requirements, which is quite often the case in ATS,
the Real-Time for Java Experts Group (RTJEG) has
created the real time specification for Java (RTSJ). The
RTSJ strengthens the semantics of thread scheduling,
memory management, and synchronization (i.e. monitor
behavior), without adding new keywords or syntactic
extensions to the existing language specification. An
improved interrupt method is specified, along with two
new classes of threads, a fixed-priority preemptive thread
scheduler, and the ability to access physical memory
directly.
With regard to synchronization, threads attempting to gain
a lock are served in strict priority order, and threads of
equal priority are served on a FIFO basis. The resulting
behavior still isn't quite the same as the Hoare monitor,
but it's certainly an improvement.
The RTSJ requires as a minimum the implementation of a
thread-scheduling algorithm with at least 28 unique
priority levels, and disallows dynamic priority changes by
the execution environment except for the case of priority
inversion avoidance. By default, the well-known priority
inheritance algorithm must be implemented to handle
priority inversion.
The memory model is extended to allow the allocation of
long- and short-lived objects outside of the heap in a
memory area. While the heap is garbage-collected as
before, memory areas are not, at least not in the traditional
sense. There are three subclasses of memory area physical memory, immortal memory, and scoped memory.
Physical memory is just what it sounds like - a specific
range of physical addresses. A typical use might be to
assign objects that require enhanced access times to static
RAM. Objects created in immortal memory persist until
the JVM terminates, regardless of how many (possibly
none) references to the object may exist at any given time.
Consequently, immortal memory is never garbage
collected. Scoped memory areas associate memory with
syntactic scope. Any use of the new operator within a
specified syntactic scope allocates from a particular
memory area. Only when the scope terminates or the
system leaves it will the system destroy any objects that
were created and call their finalizers. Multiple threads
can share a scoped memory area, and the area will remain
active until all threads have left it. Both immortal and
scoped memory areas can be assigned to physical memory
address ranges.
An RTSJ implementation would seem to be far more
suitable for ATS use than the existing Java execution
environment and API. Most if not all of the deficiencies
we have pointed out in the Java specification have been
addressed. One could view Disneyland's express line
waiting scheme as an RTSJ monitor with all express line
patrons being assigned the same priority, of a value higher
than that of the patrons in the regular line. And we
suspect that if the Dining Philosophers applet was run on
an RTSJ implementation, Duke would always have one
hand up when deadlock occurred.
The creators of the RTSJ clearly expect it to achieve very
broad acceptance due to its promise of "Write Once
Carefully, Run Everywhere Conditionally" [15]. James
Gosling, the originator of the Java language and a RTJEG
member, has co-authored a description of the RTSJ [16].
Compiled Java, which does not use a JVM, is another
approach that has found favor, particularly in embedded
applications. Compiled Java sacrifices the concept of
"write once, run everywhere" for the ability to achieve
high performance, temporally deterministic behavior on a
specific platform.
CONCLUSION
Concurrency is achievable in one form or another with
many of the traditional and newer software development
platforms used with ATS. Multi-threading, properly
implemented, provides a powerful mechanism for
implementing robust ATS software. However, due to the
inherent complexity issues involved in coordinating
threads, multi-threading should not be used simply for its
own sake, but only after careful analysis of the problem to
be solved. Implementers must have a sound grasp of the
intricacies and peculiarities of the operating system or
framework they're using in order to have any degree of
confidence that their test software will work correctly, let
alone efficiently. Future development of Java real time
implementations may allow the use of this distributed,
concurrent, and object oriented programming language in
many ATS applications.
[13] P. Jain and D. Schmidt, Experiences Converting a
C++ Communication Software Framework to Java.
Available at http://www.cs.wustl.edu.
[14] W. Stallings, Operating Systems.
1995. pp. 198-204.
Prentice-Hall,
[15] G. Bollela et al, The Real-Time Specification for
Java, Addison Wesley, June 2000. pp xxi. Available at
http://www.rtj.org
REFERENCES
[1] A. Silbershatz and P. Galvin. Operating Systems
Concepts, Addison Wesley, 1998, 5th edition. pp. 102106.
[16] G. Bollela and J. Gosling, "The Real-Time
Specification for Java", Computer, June 2000. pp. 47-54.
______________________
[2] D. Stinson, "Implementing Multi-threaded Test
Methods with ABBET/ATLAS", IEEE Autotestcon
Proceedings, 1992.
[3] J. Beveridge and R. Wiener. Multithreading
Applications in Win32. Addison-Wesley, 1997. pp. 173.
[4] B. Stroustrup, C++ Programming Language, 3rd ed.
Addison-Wesley 1997. pp. 14.
[5] A. Cohen and M. Woodring, Win32 Multithreaded
Programming. O’Reilly 1998. pp. 58.
[6] K. Fertitta and J. Harvey, "The Role of ActiveX and
COM in ATE", IEEE Autotestcon Proceedings, 1999.
[7] T. Thai, Learning DCOM, O'Reilly, 1999. pp. 148.
[8] D. Appleman, A Thread to Visual Basic. Available at
http://www.desaware.com.
[9] Sun MicroSystems, "Lesson: Doing Two or More
Tasks At Once: Threads", The Java Tutorial, 1999.
Available at http://www.java.sun.com.
[10] C.A.R. Hoare, "Monitors: An operating system
structuring concept." Comm. ACM 17, 10 (Oct.1974),
pp. 549-557.
[11] B. Lampson and D. Redell, Experience with
Processes and Monitors in Mesa, Xerox Palo Alto
Research Center, 1980. This paper is available on line at
http://www.microsoft.com
[12] International Standards Organization,
Reference Manual, ISO 8652:1995.
Ada
95
Product designations that are claimed as trademarks, to the
authors' awareness, are printed in caps or initial caps. All
trademarks are the property of their respective owners.