Guide to DECthreads


Previous | Contents

Implementation of the DECthreads tis interface library varies by DIGITAL operation 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 with the source code available should be staightforward to convert existing code that is not thread safe to become 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.8.4.

Although the C, C++, Pascal, and BLISS programming languages do not support multithreaded programming directly, compilers for those languages generally create reentrant code. However, the Fortran and COBOL languages are defined in such a way that compilers can make implicit use of static storage, and such compilers do not generate reentrant code; it is difficult to write reentrant code in a nonreentrant language. The DEC FORTRAN compiler does generate reentrant code.

4.1.2 Performance of tis Interface Routines

Routines in the DECthreads tis interface are designed to perform efficiently when called from a single-threaded environment. For example, locking a mutex is essentially just setting a bit, and unlocking the mutex requires clearing 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, even when invoked from a multithreaded environment. For a multithreaded program that uses tis routines, 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

The tis interface routines support mutexes, called tis mutexes. Like the kinds of mutexes available through the other DECthreads interfaces, tis mutexes provide synchronization between multiple threads that share resources. In fact, you can statically initialize tis mutexes using the POSIX 1003.1c standard PTHREAD_MUTEX_INITIALIZER macro (see the DECthreads pthread.h header file) or using one of the nonportable DECthreads variants. This means you can create DECthreads recursive or errorcheck mutexes if required, as well as normal mutexes.

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 reserved by DECthreads but is for use by any thread. Your program can use the global lock without calling the other DECthreads interfaces 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 POSIX.1c standard PTHREAD_COND_INITIALIZER macro or using one of the various nonportable DECthreads variants. For example, you can assign names to your tis condition variables by statically initializing them with the PTHREAD_COND_INITWITHNAME_NP 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.

Of course, your program can have more than one thread only if the DECthreads multithreading run-time environment is present. That is, if your program were to wait, there would be no other thread to "awaken" your program. Signaling or broadcasting a tis mutex when called from a single-threaded environment does nothing. This is because your program is not allowed to wait on a tis condition variable when the DECthreads multithreading run-time environment has not been initialized.

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.

4.4 Using Thread-Specific Data

The tis interface routines support the use of thread-specific data variables. If the DECthreads multithreading run-time environment is initialized, tis thread-specific data keys are transferred into that environment, so your program can continue to use the same keys.

For a program that uses tis thread-specific data in a multithreaded environment, any thread-specific data values set using a routine in the tis interface will be transferred into your program's initial thread.

4.5 Using Read-Write Locks

A read-write lock is an object that serializes access to shared information that needs to be read frequently and written only occasionally. Routines that manipulate read-write locks can control access to any shared resource and can be called by either a routine in a thread or by a routine in a thread-safe, single-threaded program.

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.

Only the tis interface offers routines that operate on read-write locks. This is not a problem because you can use tis routines in a program that uses another DECthreads interface to use its own threads.


Note

In this version of DECthreads, read-write locks are not portable---although the Single UNIX Specification, Version 2, provides a similar set of routines that will be made available in a future version of DECthreads.

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 or program 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, DECthreads gives 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. 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 allocating and initializing a 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() immediately returns [EBUSY], rather than waiting.

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.

Use the following routines to manipulate read-write locks:
Routine Description
tis_rwlock_init() Initializes a read-write lock.
tis_rwlock_destroy() Destroys a read-write lock.
tis_read_lock() Acquires a read-write lock for read access.
tis_write_lock() Acquires a read-write lock for write access.
tis_read_trylock() Attempts to acquire a read-write lock for read access without waiting.
tis_write_trylock() Attempts to acquire a read-write lock for write access without waiting.
tis_read_unlock() Unlocks a read-write lock acquired for read access.
tis_write_unlock() Unlocks a read-write lock acquired for write access.

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


Chapter 5
Using the DECthreads Exception Package

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

This chapter:

5.1 About the DECthreads Exceptions Package

The DECthreads exception package is a dynamic library and C language header file (pthread_exception.h) that together provide an interface for defining and handling exceptions. It is designed for use with the DECthreads pthread (POSIX.1c-1995) interface.

5.1.1 Supported Programming Languages

The DECthreads exception package is most useful when you are programming in the C language. The DECthreads exception package must be used with the DECthreads pthread interface.

You can use the C language exception library to catch DECthreads exceptions, but you cannot use the C++ language exception library to catch DECthreads exceptions. However, you cannot use the C language or C++ language exception library to define or raise DECthreads exceptions.

Because the DEC C++ runtime library and the POSIX thread runtime environment both use your platform's underlying exceptions mechanisms, the DEC C++ runtime library is able to run destructors an any exit from a block, including the exception handler code blocks defined using the DECthreads exception package.

5.1.2 Relation of Exceptions to Return Codes and Signals

Although the DECthreads pthread interface reports errors by returning nonzero return codes, DECthreads uses exceptions in the following cases:

5.2 Why Use Exceptions

An exception is an object that describes an error condition. Operations on exception objects allow your program to report and handle errors. If an exception can be handled properly, the program can recover from errors. For example, if an exception is raised from a parity error while reading a tape, the recovery action might be to retry reading the tape 100 times before giving up.

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

Finally, you can use the DECthreads exception package to handle thread cancelation and thread exit in a unified and modular manner. Because DECthreads implements both thread cancelation and thread exit by raising DECthreads-reserved exceptions, your code can respond to these "events" in the same modular manner as for error conditions.

5.3 Exception Programming

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

Specifically, you must:

  1. Declare one DECthreads exception object for each distinct error condition of interest to your program.
  2. Code your program to invoke the DECthreads 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 raised.
  4. Associated with each exception scope, use the DECthreads CATCH macro to define a block of exception handler code for each exception object that can be raised and to which your program must respond at this point in its work. In this code your program perform activities to repsond to a particular error condition.
  5. Associated with each exception scope, use the DECthreads CATCH_ALL macro to define an exception handler to catch any exception that your program code does not raise---that is, that can be raised by a facility that your program uses, including the host operating system itself. Because your program code does not raise 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. Associated with each exception scope, instead of defining an exception handler, use the DECthreads FINALLY macro to define finalization code, also known as epilogue code, that is always executed, regardless of whether the code in the associated exception scope raised an exception. If this code is reached, DECthreads automatically reraises the caught exception and passes it to the next outer exception scope.

When a thread in your program raises an exception, DECthreads determines whether an exception scope has been defined in the current stack frame. If so, DECthreads checks whether there is a specific handler (CATCH code block) for the raised exception or whether there is an unspecified handler (CATCH_ALL code block). If not, DECthreads 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 a DECthreads exception object in your code, your program must declare and initialize the object. A DECthreads exception object must be defined (whether explicitly or implicitly) to be of static storage class. In C language terms, the exception object can have local scope, module-wide scope, or global scope.

The next sample code fragment demonstrates declaring and initializing an exception object.

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

5.3.2 Raising an Exception

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

Example 5-1 demonstrates how to raise a DECthreads exception.

Example 5-1 Raising an Exception


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

5.3.3 Catching an Exception

After your program raises a DECthreads exception, it is passed to a location within a block of code called an 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, program terminating\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, program terminating\n"); 
      printf ("Try cleaning the heads!\n"); 
      free (local_mem); 
   } 
   CATCH_ALL { 
      free (local_mem); 
      RERAISE; 
   } 
   ENDTRY 
 

5.3.4 Reraising an Exception

Reraising an exception means to pass it to the next outer exception scope for further processing. Your program should take this step for a given exception when it must respond to the error condition but cannot completely recover from it.

As shown in Example 5-3, within a CATCH or CATCH_ALL code block, your program can invoke the RERAISE macro to pass a caught exception to the next outer exception scope in your program. If there is no next outer TRY block, the DECthreads default handler for unhandled exceptions receives the exception, produces a default error message that identifies the unhandled exception, then terminates the process.

Reraising is most appropriate for an exception caught in a CATCH_ALL block. Because this code block catches exceptions that are not "known" to your program's code, it is unlikely that your code is able to fully recover from the error condition that the exception represents.

5.3.5 Expressing Epilogue Actions

Example 5-4 demonstrates the use of the optional FINALLY block.

Example 5-4 Defining Epilogue Actions Using FINALLY


 
   int *local_mem; 
 
   local_mem = malloc (sizeof (int)); 
   TRY {                      /* An exception can be raised within this scope */ 
      operation (local_mem);  
   } 
   FINALLY { 
      free (local_mem); 
   } 
   ENDTRY 
 

A FINALLY block catches an exception and implicitly reraises the exception for the next outer exception scope to handle. The actions defined by a FINALLY block are also performed on normal exit from the TRY block without an exception being raised. This means that those actions need not be duplicated in your code.

Do not combine a FINALLY block with a CATCH block or CATCH_ALL block in the same TRY block.

5.4 Exception Objects

This section describes the attributes of DECthreads exceptions (that is, the EXCEPTION type) and the behavior of the DECthreads exception package's exception handling macros (that is, RAISE and RERAISE, TRY, CATCH and CATCH_ALL, and FINALLY).

An exception is a data object that represents an error condition that has occurred in a particular context. The error condition can be detected by the operating system, by the native programming language, by another programmatic facility that your program calls, or by your own program.

The DECthreads exception package supports several operations on exceptions. Operations on exceptions allow a program to report and handle errors. If an exception is handled properly, the program can recover from certain errors. For example, if an exception is raised from a parity error while reading a tape, the recovery action might be to retry 100 times before giving up.

5.4.1 Declaring and Initializing Exception Objects

In the DECthreads exception package, an exception is a statically allocated variable of type EXCEPTION. Declaring and initializing an exception object documents that a program reports or handles a particular error.

The EXCEPTION type is designed to be an opaque type and should only be manipulated by the DECthreads exception package routines. The actual definition of the type may differ from one DECthreads release to another. The EXCEPTION type is defined in the pthread_exception.h header file.

In general, you should declare the type as static or extern. For example:

 
   static EXCEPTION an_error; 
 

Because on some platforms an exception object may require dynamic initialization, the DECthreads exception package requires a run-time initialization call in addition to the declaration. The initialization routine is a macro named EXCEPTION_INIT. The name of the exception is passed as a parameter.

Following code fragment shows declaring and initializing an exception object:

 
   EXCEPTION parity_error;           /* Declare it */ 
 
   EXCEPTION_INIT (parity_error);    /* Initialize it */ 
 
 

5.4.2 Address Exceptions and Status Exceptions

By default, when your program raises an exception object that has been properly initialized, only an address value is passed to the current exception scope. This form of DECthreads exception object is called an address exception, because the object's value is the address in your program where an error condition was detected. Your program code that handles address exceptions is fully portable among DECthreads-supported platforms.

Use address exceptions if the error conditions that can occur in your program are not assigned to a range of status codes. Address exceptions are always unique, so using them cannot cause a "collision" with another facility's status codes and possibly lead inadvertently to handling the wrong exception.


Previous | Next | Contents