Source code

Revision control

Copy as Markdown

Other Tools

Introduction to NSPR
====================
The Netscape Portable Runtime (NSPR) API allows compliant applications
to use system facilities such as threads, thread synchronization, I/O,
interval timing, atomic operations, and several other low-level services
in a platform-independent manner. This chapter introduces key NSPR
programming concepts and illustrates them with sample code.
NSPR does not provide a platform for porting existing code. It must be
used from the beginning of a software project.
.. _NSPR_Naming_Conventions:
NSPR Naming Conventions
-----------------------
Naming of NSPR types, functions, and macros follows the following
conventions:
- Types exported by NSPR begin with ``PR`` and are followed by
intercap-style declarations, like this: :ref:`PRInt`, :ref:`PRFileDesc`
- Function definitions begin with ``PR_`` and are followed by
intercap-style declarations, like this: :ref:`PR_Read``,
:ref:`PR_JoinThread``
- Preprocessor macros begin with the letters ``PR`` and are followed by
all uppercase characters separated with the underscore character
(``_``), like this: :ref:`PR_BYTES_PER_SHORT`, :ref:`PR_EXTERN`
.. _NSPR_Threads:
NSPR Threads
------------
NSPR provides an execution environment that promotes the use of
lightweight threads. Each thread is an execution entity that is
scheduled independently from other threads in the same process. A thread
has a limited number of resources that it truly owns. These resources
include the thread stack and the CPU register set (including PC).
To an NSPR client, a thread is represented by a pointer to an opaque
structure of type :ref:`PRThread``. A thread is created by an explicit
client request and remains a valid, independent execution entity until
it returns from its root function or the process abnormally terminates.
(:ref:`PRThread` and functions for creating and manipulating threads are
described in detail in `Threads <Threads>`__.)
NSPR threads are lightweight in the sense that they are cheaper than
full-blown processes, but they are not free. They achieve the cost
reduction by relying on their containing process to manage most of the
resources that they access. This, and the fact that threads share an
address space with other threads in the same process, makes it important
to remember that *threads are not processes* .
NSPR threads are scheduled in two separate domains:
- **Local threads** are scheduled within a process only and are handled
entirely by NSPR, either by completely emulating threads on each host
operating system (OS) that doesn't support threads, or by using the
threading facilities of each host OS that does support threads to
emulate a relatively large number of local threads by using a
relatively small number of native threads.
- **Global threads** are scheduled by the host OS--not by NSPR--either
within a process or across processes on the entire host. Global
threads correspond to native threads on the host OS.
NSPR threads can also be either user threads or system threads. NSPR
provides a function, :ref:`PR_Cleanup`, that synchronizes process
termination. :ref:`PR_Cleanup` waits for the last user thread to exit
before returning, whereas it ignores system threads when determining
when a process should exit. This arrangement implies that a system
thread should not have volatile data that needs to be safely stored
away.
Priorities for NSPR threads are based loosely on hints provided by the
client and sometimes constrained by the underlying operating system.
Therefore, priorities are not rigidly defined. For more information, see
`Thread Scheduling <#Thread_Scheduling>`__.
In general, it's preferable to create local user threads with normal
priority and let NSPR take care of the details as appropriate for each
host OS. It's usually not necessary to create a global thread explicitly
unless you are planning to port your code only to platforms that provide
threading services with which you are familiar or unless the thread will
be executing code that might directly call blocking OS functions.
Threads can also have "per-thread-data" attached to them. Each thread
has a built-in per-thread error number and error string that are updated
when NSPR operations fail. It's also possible for NSPR clients to define
their own per-thread-data. For details, see `Controlling Per-Thread
Private Data <Threads#Controlling_Per-Thread_Private_Data>`__.
.. _Thread_Scheduling:
Thread Scheduling
~~~~~~~~~~~~~~~~~
NSPR threads are scheduled by priority and can be preempted or
interrupted. The sections that follow briefly introduce the NSPR
approach to these three aspects of thread scheduling.
- `Setting Thread Priorities <#Setting_Thread_Priorities>`__
- `Preempting Threads <#Preempting_Threads>`__
- `Interrupting Threads <#Interrupting_Threads>`__
For reference information on the NSPR API used for thread scheduling,
see `Threads <Threads>`__.
.. _Setting_Thread_Priorities:
Setting Thread Priorities
^^^^^^^^^^^^^^^^^^^^^^^^^
The host operating systems supported by NSPR differ widely in the
mechanisms they use to support thread priorities. In general, an NSPR
thread of higher priority has a statistically better chance of running
relative to threads of lower priority. However, because of the multiple
strategies to provide execution vehicles for threads on various host
platforms, priorities are not a clearly defined abstraction in NSPR. At
best they are intended to specify a preference with respect to the
amount of CPU time that a higher-priority thread might expect relative
to a lower-priority thread. This preference is still subject to resource
availability, and must not be used in place of proper synchronization.
For more information on thread synchronization, see `NSPR Thread
Synchronization <#NSPR_Thread_Synchronization>`__.
The issue is further muddied by inconsistent offerings from OS vendors
regarding the priority of their kernel-supported threads. NSPR assumes
that the priorities of global threads are not manageable, but that the
host OS will perform some sort of fair scheduling. It's usually
preferable to create local user threads with normal priority and let
NSPR and the host take care of the details.
In some NSPR configurations, there may be an arbitrary (and perhaps
large) number of local threads being supported by a more limited number
of **virtual processors** (an internal application of global threads).
In such situations, each virtual processor will have some number of
local threads associated with it, though exactly which local threads and
how many may vary over time. NSPR guarantees that for each virtual
processor the highest-priority, schedulable local thread is the one
executing. This thread implementation strategy is referred to as the **M
x N model.**
.. _Preempting_Threads:
Preempting Threads
^^^^^^^^^^^^^^^^^^
Preemption is the act of taking control away from a ready thread at an
arbitrary point and giving control to another appropriate thread. It
might be viewed as taking the executing thread and adding it to the end
of the ready queue for its appropriate priority, then simply running the
scheduling algorithm to find the most appropriate thread. The chosen
thread may be of higher priority, of the same priority, or even the same
thread. It will not be a thread of lower priority.
Some operating systems cannot be made preemptible (for example, Mac OS
and Win 16). This puts them at some risk in supporting arbitrary code,
even if the code is interpreted (Java). Other systems are not
thread-aware, and their runtime libraries not thread-safe (most versions
of Unix). These systems can support local level thread abstractions that
can be made preemptible, but run the risk of library corruption
(``libc``). Still other operating systems have a native notion of
threads, and their libraries are thread-aware and support locking.
However, if local threads are also present, and they are preemptible,
they are subject to deadlock. At this time, the only safe solutions are
to turn off preemption (a runtime decision) or to preempt global threads
only.
.. _Interrupting_Threads:
Interrupting Threads
^^^^^^^^^^^^^^^^^^^^
NSPR threads are interruptible, with some constraints and
inconsistencies.
To interrupt a thread, the caller of :ref:`PR_Interrupt` must have the NSPR
reference to the target thread (:ref:`PRThread`). When the target is
interrupted, it is rescheduled from the point at which it was blocked,
with a status error indicating that it was interrupted. NSPR recognizes
only two areas where a thread may be interrupted: waiting on a condition
variable and waiting on I/O. In the latter case, interruption does
cancel the I/O operation. In neither case does being interrupted imply
the demise of the thread.
.. _NSPR_Thread_Synchronization:
NSPR Thread Synchronization
---------------------------
Thread synchronization has two aspects: locking and notification.
Locking prevents access to some resource, such as a piece of shared
data: that is, it enforces mutual exclusion. Notification involves
passing synchronization information among cooperating threads.
In NSPR, a **mutual exclusion lock** (or **mutex**) of type :ref:`PRLock`
controls locking, and associated **condition variables** of type
:ref:`PRCondVar` communicate changes in state among threads. When a
programmer associates a mutex with an arbitrary collection of data, the
mutex provides a protective **monitor** around the data.
.. _Locks_and_Monitors:
Locks and Monitors
~~~~~~~~~~~~~~~~~~
In general, a monitor is a conceptual entity composed of a mutex, one or
more condition variables, and the monitored data. Monitors in this
generic sense should not be confused with the monitor type used in Java
programming. In addition to :ref:`PRLock`, NSPR provides another mutex
type, :ref:`PRMonitor`, which is reentrant and can have only one associated
condition variable. :ref:`PRMonitor` is intended for use with Java and
reflects the Java approach to thread synchronization.
To access the data in the monitor, the thread performing the access must
hold the mutex, also described as being "in the monitor." Mutual
exclusion guarantees that only one thread can be in the monitor at a
time and that no thread may observe or modify the monitored data without
being in the monitor.
Monitoring is about protecting data, not code. A **monitored invariant**
is a Boolean expression over the monitored data. The expression may be
false only when a thread is in the monitor (holding the monitor's
mutex). This requirement implies that when a thread first enters the
monitor, an evaluation of the invariant expression must yield a
``true``. The thread must also reinstate the monitored invariant before
exiting the monitor. Therefore, evaluation of the expression must also
yield a true at that point in execution.
A trivial example might be as follows. Suppose an object has three
values, ``v1``, ``v2``, and ``sum``. The invariant is that the third
value is the sum of the other two. Expressed mathematically, the
invariant is ``sum = v1 + v2``. Any modification of ``v1`` or ``v2``
requires modification of ``sum``. Since that is a complex operation, it
must be monitored. Furthermore, any type of access to ``sum`` must also
be monitored to ensure that neither ``v1`` nor ``v2`` are in flux.
.. note::
Evaluation of the invariant expression is a conceptual
requirement and is rarely done in practice. It is valuable to
formally define the expression during design, write it down, and
adhere to it. It is also useful to implement the expression during
development and test it where appropriate. The thread makes an
absolute assertion of the expression's evaluation both on entering
and on exiting the monitor.
Acquiring a lock is a synchronous operation. Once the lock primitive is
called, the thread returns only when it has acquired the lock. Should
another thread (or the same thread) already have the lock held, the
calling thread blocks, waiting for the situation to improve. That
blocked state is not interruptible, nor is it timed.
.. _Condition_Variables:
Condition Variables
~~~~~~~~~~~~~~~~~~~
Condition variables facilitate communication between threads. The
communication available is a semantic-free notification whose context
must be supplied by the programmer. Conditions are closely associated
with a single monitor.
The association between a condition and a monitor is established when a
condition variable is created, and the association persists for the life
of the condition variable. In addition, a static association exists
between the condition and some data within the monitor. This data is
what will be manipulated by the program under the protection of the
monitor. A thread may wait on notification of a condition that signals
changes in the state of the associated data. Other threads may notify
the condition when changes occur.
Condition variables are always monitored. The relevant operations on
conditions are always performed from within the monitor. They are used
to communicate changes in the state of the monitored data (though still
preserving the monitored invariant). Condition variables allow one or
more threads to wait for a predetermined condition to exist, and they
allow another thread to notify them when the condition occurs. Condition
variables themselves do not carry the semantics of the state change, but
simply provide a mechanism for indicating that something has changed. It
is the programmer's responsibility to associate a condition with the
state of the data.
A thread may be designed to wait for a particular situation to exist in
some monitored data. Since the nature of the situation is not an
attribute of the condition, the program must test that itself. Since
this testing involves the monitored data, it must be done from within
the monitor. The wait operation atomically exits the monitor and blocks
the calling thread in a waiting condition state. When the thread is
resumed after the wait, it will have reentered the monitor, making
operations on the data safe.
There is a subtle interaction between the thread(s) waiting on a
condition and those notifying it. The notification must take place
within a monitor--the same monitor that protects the data being
manipulated by the notifier. In pseudocode, the sequence looks like
this:
.. code::
enter(monitor);
... manipulate the monitored data
notify(condition);
exit(monitor);
Notifications to a condition do not accumulate. Nor is it required that
any thread be waiting on a condition when the notification occurs. The
design of the code that waits on a condition must take these facts into
account. Therefore, the pseudocode for the waiting thread might look
like this:
.. code::
enter(monitor)
while (!expression) wait(condition);
... manipulate monitored data
exit(monitor);
The need to evaluate the Boolean expression again after rescheduling
from a wait may appear unnecessary, but it is vital to the correct
execution of the program. The notification promotes a thread waiting on
a condition to a ready state. When that thread actually gets scheduled
is determined by the thread scheduler and cannot be predicted. If
multiple threads are actually processing the notifications, one or more
of them could be scheduled ahead of the one explicitly promoted by the
notification. One such thread could enter the monitor and perform the
work indicated by the notification, and exit. In this case the thread
would resume from the wait only to find that there's nothing to do.
For example, suppose the defined rule of a function is that it should
wait until there is an object available and that it should return a
reference to that object. Writing the code as follows could potentially
return a null reference, violating the invariant of the function:
.. code::
void *dequeue()
{
void *db;
enter(monitor);
if ((db = delink()) == null)
{
wait(condition);
db = delink();
}
exit(monitor);
return db;
}
The same function would be more appropriately written as follows:
.. code::
void *dequeue()
{
void *db;
enter(monitor);
while ((db = delink()) == null)
wait(condition);
exit(monitor);
return db;
}
.. note::
**Caution**: The semantics of :ref:`PR_WaitCondVar` assume that the
monitor is about to be exited. This assumption implies that the
monitored invariant must be reinstated before calling
:ref:`PR_WaitCondVar`. Failure to do this will cause subtle but painful
bugs.
To modify monitored data safely, a thread must be in the monitor. Since
no other thread may modify or (in most cases) even observe the protected
data from outside the monitor, the thread can safely make any
modifications needed. When the changes have been completed, the thread
notifies the condition associated with the data and exits the monitor
using :ref:`PR_NotifyCondVar`. Logically, each such notification promotes
one thread that was waiting on the condition to a ready state. An
alternate form of notification (:ref:`PR_NotifyAllCondVar`) promotes all
threads waiting on a condition to the ready state. If no threads were
waiting, the notification is a no-op.
Waiting on a condition variable is an interruptible operation. Another
thread could target the waiting thread and issue a :ref:`PR_Interrupt`,
causing a waiting thread to resume. In such cases the return from the
wait operation indicates a failure and definitively indicates that the
cause of the failure is an interrupt.
A call to :ref:`PR_WaitCondVar` may also resume because the interval
specified on the wait call has expired. However, this fact cannot be
unambiguously delivered, so no attempt is made to do so. If the logic of
a program allows for timing of waits on conditions, then the clock must
be treated as part of the monitored data and the amount of time elapsed
re-asserted when the call returns. Philosophically, timeouts should be
treated as explicit notifications, and therefore require the testing of
the monitored data upon resumption.
.. _NSPR_Sample_Code:
NSPR Sample Code
----------------
The documents linked here present two sample programs, including
detailed annotations: ``layer.html`` and ``switch.html``. In addition to
these annotated HTML versions, the same samples are available in pure
source form.