Tru64 UNIX
Guide to the POSIX Threads Library


Previous Contents Index


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. The Threads Library provides the thread-independent services (or tis) interface to help you write 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 easy to maintain. 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 Threads Library synchronization.

The guidelines for using the 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 tis interface are:

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

It is not difficult to create thread-safe code using the 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 the Threads Library. 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 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, the Threads Library automatically revectors the program's run-time linkages to most tis routines. This allows subsequent calls to those routines to use the normal 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 Threads Library operation is transparent to library code that uses the tis interface.

The tis interface deliberately provides no way to determine whether the Threads Library is active within the process. Thread-safe code should always act as if multiple threads can be active.

4.1.4 Cancelation Points

The following routines in the tis interface are cancelation points:

tis_cond_wait()
tis_testcancel()

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 Threads Library 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.

Operations on the global lock are also supported by tis interface routines. The global lock is a recursive mutex that is provided by the Threads Library 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 Threads Library 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 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.


Chapter 5
Using the Exceptions Package

This chapter describes how to use the exceptions package and demonstrates conventions for the modular use of exceptions in a multithreaded program.

This chapter does the following:

5.1 About the Exceptions Package

The exceptions package is a part of the POSIX Threads Library. A C language header file ( pthread_exception.h ) provides an interface for defining and handling exceptions. It is designed for use with the pthreads interface routines. If you use the exceptions package, your application must be linked with the Threads Library.

5.1.1 Supported Programming Languages

You can use the exceptions package only when you are programming in the C language. While the exceptions will compile under C++, they will not behave properly. In addition, gcc lacks the Compaq C extensions that are needed to interact with the native exception handling system, and will not interoperate correctly with other language exception facilities.

You can use the C language exception handling mechanism (SEH) to catch exceptions. You can catch exceptions in C++ using catch(...) , and propagation of exceptions will run C++ object destructors. Currently, C++ code cannot catch specific exceptions. Also, CATCH , CATCH_ALL and FINALLY clauses will not run when C++ code raises an exception. (These restrictions will be reduced or removed in a future release.)

5.1.2 Relation of Exceptions to Return Codes and Signals

The Threads Library uses exceptions in the following cases:

5.2 Why Use Exceptions

An exception is a mechanism for reporting an error condition. An exception is represented by an exception subject. Operations on exception objects allow your program to report and handle errors. If your program can handle an exception properly, the program can recover from errors. For example, while reading a tape, a program raises an exception from a parity error. The recovery action might be to retry reading the tape 100 times before giving up. However, if the program does not handle the exception, then the program terminates. Reporting errors via exceptions ensures that the error will not inadvertently go unnoticed and cause problems later.

You use exception programming to identify a portion of a routine, called an exception scope, where a calling thread wishes to respond to particular error conditions or perhaps to any error condition. The thread can respond to each exception in either of two ways:

As a result, you can use the exceptions package to handle thread cancelation and thread exit in a unified and modular manner. Because the Threads Library implements both thread cancelation and thread exit by raising exceptions, your code can respond to these events in the same modular manner as it does for error conditions.

5.3 Exception Programming

Each exception object is of the EXCEPTION type, which is defined in the pthread_exception.h header file.

To use exceptions, do the following:

  1. Declare one exception object for each distinct error condition of interest to your program.
  2. Code your program to invoke the RAISE macro when it detects an error condition.
  3. Code an exception scope, using the TRY and ENDTRY macros, to define the program scope within which an exception might be handled.
  4. Optionally include the CATCH macro, which is associated with each exception scope, to define a block of exception handler code for each exception that your program wishes to handle at this point in its work. In this block your program can perform activities to respond to the particular error condition.
  5. Optionally include the CATCH_ALL macro, which is associated with each exception scope, to define an exception handler to catch any other exception that might be raised, if your code needs to respond to such errors. Unless your code can fully recover from these exceptions, your handler code must also reraise the caught exception so that the next outer exception scope also has the chance to respond to it.
  6. Use the FINALLY macro, which is associated with each exception scope, to define finalization code, also known as epilogue code. This code is always executed when control leaves the TRY block, regardless of whether the code in the associated exception scope raised an exception. If this code is reached because of an exception being raised, the Threads Library automatically reraises the caught exception and passes it to the next outer exception scope.

When a thread in your program raises an exception, the Threads Library determines whether an exception scope has been defined in the current stack frame. If so, the Threads Library checks whether there is either a specific handler ( CATCH code block) for the raised exception or an unspecified handler ( CATCH_ALL or FINALLY code block). If not, the Threads Library passes the raised exception to the next outer exception scope that does contain the pertinent code block. Thread execution resumes at that block. Attempting to catch a raised exception can cause a thread's stack to be unwound one or more call frames.

An exception can be caught only by the thread in which it is raised. An exception does not propagate from one thread to another.

5.3.1 Declaring and Initializing an Exception

Before referring to an exception object in your code, your program must declare and initialize the object. You must define an exception object (whether explicitly or implicitly) to be of static storage class.

The next sample code fragment demonstrates how a program declares and initializes an exception object.


 
   static EXCEPTION parity_error;  /* Declare the exception */ 
 
   EXCEPTION_INIT (parity_error);  /* Initialize the exception */ 
 

5.3.2 Raising an Exception

Raise an exception to indicate that your program has detected an error condition in response to which the program must take some action. Your program raises the exception by invoking the RAISE macro.

Example 5-1 demonstrates how to raise an exception.

Example 5-1 Raising an Exception

 
   static EXCEPTION parity_error; 
 
   int  read_tape(void) 
   { 
      int ret; 
 
      EXCEPTION_INIT (parity_error);     /* Initialize it */ 
      if (tape_is_ready) { 
         ret = read(tape_device); 
         if (ret = BAD_PARITY) 
            RAISE (parity_error);           /* Raise it */ 
      }                                       
   } 
 

5.3.3 Catching an Exception

After your program raises an exception, it is passed to a location within a block of code in a containing exception scope. The exception scope defines:

Example 5-2 shows a TRY code block with a CATCH code block defined to catch the exception object named parity_error when it is raised within the read_tape() routine.

Example 5-2 Catching an Exception Using CATCH

 
   TRY { 
      read_tape (); 
   } 
   CATCH (parity_error) { 
      printf ("Oops, parity error, read aborted\n"); 
      printf ("Try cleaning the heads!\n"); 
   } 
   ENDTRY
 

Example 5-3 demonstrates how CATCH and CATCH_ALL code blocks work together to handle different raised exceptions within a given TRY code block.

Example 5-3 Catching an Exception Using CATCH and CATCH_ALL

 
   int *local_mem; 
 
   local_mem = malloc (sizeof (int)); 
   TRY {                /* An exception can be raised within this scope */ 
      read_tape (); 
      free (local_mem); 
   } 
   CATCH (parity_error) { 
      printf ("Oops, parity error, read aborted\n"); 
      printf ("Try cleaning the heads!\n"); 
      free (local_mem); 
   } 
   CATCH_ALL { 
      free (local_mem); 
      RERAISE; 
   } 
   ENDTRY 
 


Previous Next Contents Index