Guide to the POSIX Threads Library
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:
- Synchronization without linking the thread library with some
unique routines and some routines that correspond to those in the
pthread interface
- Common synchronization data types (such as mutexes and condition
variables) with the pthread interface
- Unique tis synchronization objects (such as the
read-write lock which is different from the pthread
read-write lock
- Support for thread-specific data objects
Implementation of the 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
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. For instance, if the Threads
Library 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 the Threads Library 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 the Threads Library can be dynamically activated
into the process at any time.
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.
If the 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 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:
- Describes the exceptions package
- Shows how to declare, initialize, and handle an exception object
in your program
- Describes the exceptions package's macros that support exception
handling
- Describes the exceptions package's API-level routines that operate
on exception objects
- Lists the names of exception objects that the exceptions package
defines for its own use
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:
- The
pthread_exit()
routine raises the exception
pthread_exit_e
defined by the Threads Library.
- Canceling a thread causes the Threads Library to raise the
exception
pthread_cancel_e
defined by the Threads Library.
- On Tru64 UNIX, synchronous signals (such as SIGSEGV) are converted
to exceptions unless a signal action is declared.
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:
- Catch the exception. This means that the code handles all
effects of the error condition from within the exception scope, not
from the point where the exception was raised.
- Finalize the exception scope. This means that the current
scope's context is cleaned up and resources (such as mutexes) are
released. The exception is then passed to the next outer exception
scope for further processing. The exception package supports
finalization of a scope even when no exception was raised, so that
resources are always released without duplication of code.
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:
- Declare one exception object for each distinct error condition of
interest to your program.
- Code your program to invoke the
RAISE
macro when it detects an error condition.
- Code an exception scope, using the
TRY
and
ENDTRY
macros, to define the program scope within which an exception might be
handled.
- 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.
- 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.
- 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:
- A
TRY
code block, a lexical scope within which an exception will be handled
if it is raised (if there is a matching
CATCH
block or a
CATCH_ALL
or
FINALLY
block).
- (Optionally) A
CATCH
code block, where your program handles a particular exception that was
raised within the scope of this
TRY
block (a single
TRY
block may have more than one
CATCH
block for different exceptions).
- (Optionally) A
CATCH_ALL
code block, where your program handles any exception raised within the
scope of this
TRY
block that is not named as an argument in a preceding
CATCH
block in this
TRY
block. The
CATCH_ALL
block must be the last block, following any
CATCH
blocks. (Only one
CATCH_ALL
block may be associated with a
TRY
block.)
- (Optionally) A
FINALLY
code block, where your program performs finalization, or epilogue,
actions at the end of the
TRY
block, whether an exception was raised or not (an exception scope with a
FINALLY
block cannot also have either a
CATCH
or
CATCH_ALL
block).
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
|
|