Tru64 UNIX Guide to DECthreads


Previous Contents Index

3.7.6 Identifying Possible Word-Tearing Situations Using Visual Threads

For Tru64 UNIX systems, the Visual Threads tool can warn the developer at application run-time that a possible word-tearing situation has been detected. Enable the UnguardedData rule before running the application. This rule causes Visual Threads to track whether any memory location (i.e. granule) in the application has been accessed from two threads without proper synchronization. This includes detection of word tearing as well as more straightforward synchronization errors. See the Visual Threads product's online help for more information.

Visual Threads is available as part of the Developer's Toolkit for Tru64 UNIX.

3.8 One-Time Initialization

Your program might have one or more routines that must be executed before any thread executes code in your facility, but that must be executed only once, regardless of the sequence in which threads start executing. For example, your program can initialize mutexes, condition variables, or thread-specific data keys---each of which must be created only once---in a one-time initialization routine.

You can use the pthread_once() routine to ensure that your program's initialization routine executes only once---that is, by the first thread that attempts to initialize your program's resources. Multiple threads can attempt to call the program initialization routine via the pthread_once() routine, and DECthreads ensures that the specified initialization routine is called only once.

On the other hand, rather than use the pthread_once() routine, your program could statically initialize a mutex and a flag, then simply lock the mutex and test the flag. In many cases, this technique might be more straightforward to implement.

Finally, you can use implicit (and nonportable) initialization mechanisms, such as OpenVMS LIB$INITIALIZE, Tru64 UNIX dynamic loader __init_ code.

3.9 Managing Dependencies Upon Other Libraries

Because multithreaded programming has become common only recently, many existing code libraries are incompatible with multithreaded uses. For example, many traditional run-time library routines maintain state across multiple calls using static storage. This storage can become corrupted if routines are called from multiple threads at the same time. Even if the calls from multiple threads are serialized, code that depends upon a sequence of return values might not work.

For example, the UNIX getpwent(2) routine returns the entries in the password file in sequence. If multiple threads call getpwent(2) repeatedly, even if the calls are serialized, no thread will obtain all entries in the password file. (This is not a problem on Tru64 UNIX, because the state is maintained using thread-specific data.)

Different library routines are be compatible with multithreaded programming to different extents. The important distinctions are thread reentrancy and thread safety.

3.9.1 Thread Reentrancy

A routine is reentrant if it can be used simultaneously when called by different threads. For example, the standard C run-time library routine strtok() can be made reentrant most efficiently by adding an argument that specifies a context for the sequence of tokens. Thus, multiple threads can simultaneously parse different strings without interfering with each other.

A reentrant routine should have no dependency on static data. Because access to static data must be synchronized, there is always a performance penalty due to the cost of synchronizing also in the loss of potential parallelism throughout the program. A routine that does not use any data that is shared between threads can proceed without locking.

If you are developing new interfaces, make sure that any persistent context information (like the last-token-returned pointer in strtok()) is passed explicitly so that multiple threads can process independent streams of information independently. Return information to the caller through routine values, output parameters (where the caller passes the address and length of a buffer), or by allocating dynamic memory and requiring the caller to free that memory when finished. Avoid using errno or other global variables for returning error or diagnostic information; use routine return values instead.

3.9.2 Thread Safety

A routine is thread-safe if it can be called simultaneously from multiple threads without risk of corruption. If the routine is not actually reentrant, generally this means that it does some level of locking to prevent simultaneously active calls in different threads.

Thread-safe routines tend to be less efficient than reentrant routines. For example, a package that is thread-safe might still block all threads in the process while one thread executes the code.

Routines such as localtime() or strtok(), which traditionally rely on static storage, can be made thread-safe by using thread-specific data instead of static variablesas is done on Tru64 UNIX. This prevents corruption and avoids the overhead of synchronization. However, using thread-specific data is not without its own cost, and it is not always the best solution. Using an alternate, reentrant version of the routine, such as the POSIX strtok_r() interface, is often preferable.

3.9.3 Lacking Thread Safety

When your program must call a routine that is not thread-safe, your program must ensure serialization and exclusivity of the unsafe routine across all threads in the program.

If a routine is not specifically documented as reentrant or thread safe, you are most safe to assume that it is not safe to use as-is with your multithreaded program. Never assume that a routine is fully thread safe unless it is expressly documented as such; a routine can use static data in ways that are not obvious from its interface. A routine carefully written to be thread-safe but that calls some other routine that is not thread-safe without proper protection, is itself not thread safe.

3.9.3.1 Using Mutex Around Call to Unsafe Code

Holding a mutex while calling any unsafe code accomplishes this. All threads and libraries using the routine should use the same mutex. Note that even if two libraries carefully lock a mutex around every call to a given routine, if each library uses a different mutex, the routine is not protected against multiple simultaneous calls from different libraries.

Note that your program might be required to protect a series of calls, rather than just a single call, to routines that are not thread safe.

3.9.3.2 Using the DECthreads Global Lock

To ensure serialization and exclusivity of the unsafe code, DECthreads provides one global lock that can be used by all threads in a program when calling routines or code that is not thread-safe while already holding the lock. Because there is only one global lock, you do not need to fully analyze all of the dependencies in unsafe code that your program calls.

Acquire the global lock by calling pthread_lock_global_np(); release the global lock by calling pthread_unlock_global_np().

The global lock allows a thread to acquire the lock recursively, so that you do not need to be concerned if you call a routine that also may acquire the global lock.

Use the global lock whenever calling unsafe routines. All DECthreads routines are thread-safe.

3.9.3.3 Using or Copying Static Data Before Releasing the Mutex

In many cases your program must protect more than just the call itself to a routine that is not thread-safe. Your program must use or copy any static return values before releasing the mutex that is being held.

3.9.4 Use of Multiple Threads Libraries Not Supported

DECthreads performs user-mode execution context-switching within a process by exchanging register sets, including the program counter and stack pointer. If any other code within the process also performs this sort of context switch, neither DECthreads nor that other code can ever know the proper identity of the context which is active at any time. This can result in, at best, unpredictable behavior---and, at worst, severe errors.

For example, under OpenVMS VAX, the VAX Ada run-time library provides its own tasking package that does not use DECthreads scheduling. Therefore, VAX Ada tasking cannot be used within a process that also uses DECthreads. (This restriction does not exist for Compaq Ada for Tru64 UNIX or for OpenVMS Alpha, because it uses DECthreads.)

3.10 Detecting DECthreads Error Conditions

DECthreads can detect some of the following types of errors:

The DECthreads pthread interface reports API errors by returning an integer value indicating the type of error.

DECthreads internal errors result in a bugcheck. DECthreads writes a message that summarizes the problem to the process's current error device, and (on OpenVMS) writes a file that contains more detailed information. (On Tru64 UNIX systems, the core file is sufficient for analysis of the process using the Ladebug debugger.)

By default, the file is named pthread_dump.log and is created in the process's current (or default) directory. To cause DECthreads to write the bugcheck information into a different file, define PTHREAD_CONFIG and set its dump= major keyword. (See Section C.1 for more information about using PTHREAD_CONFIG.)

If DECthreads cannot create the specified file when it performs the bugcheck, it will try to create the default file. If it cannot create the default file, it will write the detailed information to the error device.

3.10.1 DECthreads Bugcheck Information

The header message written to the error device starts with a line reporting that DECthreads has detected an internal problem and that it is terminating execution. It also includes the version of the DECthreads library. The message resembles this:


 
   %DECthreads bugcheck (version V3.13-180), terminating execution. 
 

The next line states the reason for the failure. On Tru64 UNIX, this is followed by process termination with SIGABRT (SIGIOT), which causes writing of a core dump file. On other platforms, a final line on the error device specifies the location of the file that contains detailed state information produced by DECthreads, as in the following example:


 
   % Dumping to pthread_dump.log 
 

The detailed information file contains information that is usually necessary to track down the problem.

3.10.2 Interpreting a DECthreads Bugcheck

The fact that DECthreads terminated the process with a bugcheck can mean that some subtle problem in DECthreads has been uncovered. However, DECthreads does not report all possible API errors, and there are a number of ways in which incorrect code in your program can lead to a DECthreads bugcheck.

A common example is the use of any mutex operation or of certain condition variable operations from within an interrupt routine (that is, a Tru64 UNIX signal handler or OpenVMS AST routine). This type of programming error most commonly results in a bugcheck that reports an "krnSpinLockPrm: deadlock detected" message or a "Can't find null thread" message. To prevent this type of error, do not use DECthreads routines other than those with the _int suffix in their names, such as pthread_cond_signal_int_np() from an interrupt routine.

In addition, DECthreads maintains a variety of state information in memory which can be overwritten by your own code. Therefore, it is possible for an application to accidentally modify DECthreads state by writing through invalid pointers, which can result in a bugcheck or other undesirable behavior.

If you encounter a DECthreads bugcheck, first check your application for memory corruptions, calls from AST routines, etc., and then contact your Compaq support representative and include this information file (or the Tru64 UNIX core file) along with sample code and output. Always include the full name and version of the operating system, and any patches that have been installed. If complete version information is lacking, useful core file analysis might not be possible.


Chapter 4
Writing Thread-Safe Libraries

A thread-safe library consists of routines that are coded so that they are safe to be called from applications that use threads. DECthreads provides the thread-independent services (or tis) interface to support writing efficient, thread-safe code that does not itself use threads.

When called by a single-threaded program, the tis interface provides thread-independent synchronization services that are low overhead. For instance, tis routines avoid the use of interlocked instructions and memory barriers.

When called by a multithreaded program, the tis routines also provide full support for DECthreads synchronization.

The guidelines for using the DECthreads pthread interface routines also apply to using the corresponding tis interface routine in a multithreaded environment.

4.1 Features of the tis Interface

Among the key features of the DECthreads tis interface are:

Implementation of the DECthreads tis interface library varies by Compaq operating system. For more information, see this guide's operating system-specific appendixes.

It is not difficult to create thread-safe code using the DECthreads tis interface, and it should be straightforward to modify existing source code that is not thread-safe to make it thread-safe.

4.1.1 Reentrant Code Required

Your first consideration is whether the language compiler used in translating the source code produces reentrant code. Most Ada compilers generate inherently reentrant code because Ada supports multithreaded programming. On OpenVMS VAX systems, there are special restrictions on using the VAX Ada compiler to produce code or libraries to be interfaced with DECthreads. See Section 3.9.4.

Although the C, C++, Pascal, BLISS, FORTRAN and COBOL programming languages do not support multithreaded programming directly, compilers for those languages generally create reentrant code.

4.1.2 Performance of tis Interface Routines

Routines in the DECthreads tis interface are designed to impose low overhead when called from a single-threaded environment. For example, locking a mutex is essentially just setting a bit, and unlocking the mutex clears the bit.

4.1.3 Run-Time Linkage of tis Interface Routines

All operations of tis interface routines require a call into the tis library. During program initialization, DECthreads automatically revectors the program's run-time linkages to most tis routines. This allows subsequent calls to those routines to use the normal DECthreads multithreaded (and SMP-safe) operations.

After the revectoring of run-time linkages has occurred, for example, a call to tis_mutex_lock() operates exactly as if pthread_mutex_lock() had been called. Thus, the transition from tis stubs to full DECthreads operation is transparent to library code that uses the tis interface. For instance, if DECthreads is dynamically activated while a tis mutex is acquired, the mutex can be released normally.

The tis interface deliberately provides no way to determine whether DECthreads is active within the process. Thread-safe code should always act as if multiple threads can be active. To do otherwise inevitably results in incorrect program behavior, given that DECthreads can be dynamically activated into the process at any time.

4.1.4 Cancelation Points

The following routines in the DECthreads tis interface are cancelation points:

However, because the tis interface has no mechanism for requesting thread cancelation, no cancelation requests are actually delivered in these routines unless threads are present at run-time.

4.2 Using Mutexes

Like the mutexes available through the other pthread interface, tis mutexes provide synchronization between multiple threads that share resources. In fact, you can statically initialize tis mutexes using the PTHREAD_MUTEX_INITIALIZER macro (see the DECthreads pthread.h header file).

You can assign names to your program's tis mutexes by statically initializing them with the PTHREAD_MUTEX_INITWITHNAME_NP macro.

Unlike static initialization, dynamic initialization of tis mutexes is limited due to the absence of support for mutex attributes objects among tis interface routines. Thus, for example, the tis_mutex_init() routine can create only normal mutexes.

If the DECthreads multithreading run-time environment becomes initialized dynamically, any tis mutexes acquired by your program remain acquired. The ownership of recursive and errorcheck mutexes remains valid.

Operations on the DECthreads global lock are also supported by tis interface routines. The DECthreads global lock is a recursive mutex that is provided by DECthreads for use by any thread. Your program can use the global lock without calling the pthread interface by calling tis_lock_global() and tis_unlock_global().

4.3 Using Condition Variables

Tis condition variables behave like condition variables created using the pthread interface. You can initialize them statically using the PTHREAD_COND_INITIALIZER macro.

As for tis mutexes, dynamic initialization of tis condition variables is limited due to the absence of support for condition variable attributes objects among tis interface routines.

A condition variable wait is useful only when there are other threads. Your program can have more than one thread only if the DECthreads multithreading run-time environment is present. In a non-threaded environment, a wait aborts and signaling or broadcasting a tis mutex does nothing.

For code in a thread-safe library that uses a condition variable, construct its wait predicate so that the code does not actually require a block on the condition variable when called in a single-threaded environment. Please see the tis_io_complete() and tis_sync() reference pages.

4.4 Using Thread-Specific Data

The tis interface routines support the use of thread-specific data. If code in the process creates keys or sets thread-specific data values before the DECthreads multithreading run-time environment is initialized, those keys and values continue to be available to your program in the initial thread.

4.5 Using Read-Write Locks

A read-write lock is an object that allows the application to control access to information that can be read concurrently by more than one thread and that needs to be read frequently and written only occasionally. Routines that manipulate the tis interface's read-write lock objects can control access to any shared resource.

For example, in a cache of recently accessed information, many threads can simultaneously examine the cache without conflict. When a thread must update the cache, it must have exclusive access.

Tis read-write locks are completely different from the newer pthread read-write locks. Currently, the latter have no tis equivalent.

Your program can acquire a read-write lock for shared read access or for exclusive write access. An attempt to acquire a read-write lock for read access will block when any thread has already acquired that lock for write access. An attempt to acquire a read-write lock for write access will block when another thread has already acquired that lock for either read or write access.

In a multithreaded environment, when both readers and writers are waiting at the same time for access via an already acquired read-write lock, tis read-write locks give precedence to the readers when the lock is released. This policy of "read precedence" favors concurrency because it potentially allows many threads to accomplish work simultaneously. (Note that this differs from pthread read-write locks, which have writer precedence.) Figure 4-1 shows a read-write lock's behavior in response to three threads (one writer and two readers) that must access the same memory object.

Figure 4-1 Read-Write Lock Behavior


The tis_rwlock_init() routine initializes a read-write lock by initializing the supplied tis_rwlock_t structure.

Your program uses the tis_read_lock() or tis_write_lock() routine to acquire a read-write lock when access to a shared resource is required. tis_read_trylock() and tis_write_trylock() can also be called to acquire a read-write lock. Note that if the lock is already acquired by another caller, tis_read_trylock() and tis_write_trylock() immediately return [EBUSY], rather than waiting.

If a non-threaded program manes a tis call that would block (such as a call to tis_cond_wait(), tis_read_lock() or tis_write_lock()), it is a fatal error that will abort the program.

Your program calls the tis_rwlock_destroy() routine when it is finished using a read-write lock. This routine frees the lock's resources for re-use.

For more information about each tis interface routine that manipulates a read-write lock, see Part 3.


Previous Next Contents Index