Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
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.