Verrsion 1.0
Copyright © 2003–2005 Iowa State University
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with the Invariant Sections being Appendix B, GNU Free Documentation License, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in Appendix B, GNU Free Documentation License.
$Date: 2005-06-27 15:45:57 -0500 (Mon, 27 Jun 2005) $
Table of Contents
For those developers new to the VR Juggler Portable Runtime (VPR), VPR provides an cross-platform, object-oriented abstraction layer to common operating system features. VPR is the key to the portability of Gadgeteer, Tweek, VR Juggler, and other middleware written at the Virtual Reality Applications Center. It has been in development since January 1997, and it has grown to be a highly portable, robust tool. Software written on top of VPR can be compiled on IRIX, Linux, Windows, FreeBSD, and Solaris, usually without modification.
Internally, VPR wraps platform-specific APIs such as BSD sockets, POSIX threads, and Win32 overlapped I/O. Depending upon how it is compiled, it may also wrap the Netscape Portable Runtime (NSPR), another cross-platform OS abstraction layer written in C. By wrapping NSPR, VPR provides developers with an object-oriented interface and gains even better portability. These details are all hidden behind the classes that make up VPR, and users of VPR do not need to worry about platform-specific details as a result.
VPR is basically a collection of utility classes. As such, the biggest part of using VPR is knowing the interface for a given class. In this book, we provide high-level information about various pieces of VPR in hopes of making VPR easier to use. The book itself is designed so that readers can focus on what they need to know about VPR classes. For example, someone who wants to learn about using the VPR thread abstraction can go straight to that part of the book (i.e., Part II, “Multi-Threading”). Within each part, however, the chapters build up the concepts incrementally, so it is advisable, for example, to understand the basics of VPR I/O before trying to learn about the serial port abstraction.
To begin, we will cover the components of VPR that will be used for I/O programming. This includes how to use VPR sockets and serial ports. We assume that the reader has at least some familiarity with operating system programming, in particular with serial device I/O and socket I/O.
Table of Contents
Table of Contents
One of the largest components of VPR is its I/O abstraction. All
I/O classes (file handles, serial ports, and sockets) share the base
class vpr::BlockIO. Reads and writes are performed using contiguous blocks
of memory (buffers). This design provides an API that closely resembles
that of the underlying operating system (with methods called
read() and write()),
but it is in contrast to stream-oriented I/O that is usually seen in
C++. Streams could be written on top of the buffered I/O classes, but
thus far, the need has not arisen. With this in mind, the design
provides an API that is immediately familiar to programmers used to
POSIX-based interfaces, but the API may seem clumsy to C++ programmers
who are accustomed to using std::ostream and
friends.
Readers interested in the implementation of the I/O component of VPR are referred to Appendix A, I/O Implementation Information. We discuss the use of the VPR socket abstraction, and we provide some insight into how the abstraction is implemented. By providing some implementation details, it is our hope that the online API reference will be easier to understand and navigate.
Opening and closing I/O devices is quite simple. There are two
methods for performing these actions:
vpr::BlockIO::open() and
vpr::BlockIO::close(). However, at the
vpr::BlockIO level, these methods are pure
virtual (i.e., abstract), and thus, the implementation varies
depending on the actual I/O device, be it a socket, serial port, or
file descriptor. Regardless of the implementation, the preconditions
for vpr::BlockIO::open() state that the
device must not already be open. For
vpr::BlockIO::close(), the device must be
open before an attempt is made to close it.
Prior to opening an I/O device, some attributes can be set.
These in turn affect how the device is opened. In the general case
of vpr::BlockIO, the only attribute that is available determines
whether the device will be opened in blocking mode or non-blocking
mode. By default, all devices open in blocking mode, and in most
cases, this is the desired mode.
The decision to use blocking or non-blocking I/O depends on
the needs of the application or library being developed on top of
VPR. While the decision can be made before opening the device, it
can also be made after the device is open using the methods
vpr::BlockIO::enableBlocking() and
vpr::BlockIO::enableNonBlocking().
Typically, the blocking/non-blocking state should be set exactly
once (either before or after opening the device). In some cases, it
is not possible to change the state after a critical “point of
no return.” Refer to the section called “Fixed Blocking State” for more information on
this.
Reads and writes occur using the read()
and write() methods respectively. These
methods are overloaded for common data structures that may be used to
store the information being read or written. For example, strings are
used frequently in I/O handling, so the type std::string can be used
for easy management of string data. When reading n
bytes, the std::string object will be resized
internally by read() to ensure that it has
enough room to store the full buffer. The same is true for the
read() variant that takes a
std::vector<vpr::Unit8> object reference.
This overloaded version of read() is helpful
when dealing in arrays of bytes. Of course, the lowest level variant
of read() is the version that takes a
void* buffer. In this case, the buffer pointed to must
have at least n bytes of contiguous storage prior
to calling read().
There is also a special method called
readn() that guarantees that
n bytes will be read. (The
read() method only guarantees that it read
at most n bytes.) As such,
readn() is a blocking call, even when a
non-blocking data source is being used behind the scenes. It will not
return until all n bytes have been read or an error
occurs while reading.
Writing to an I/O object works as one might expect. The same
overloads are available for write() as are
available for read() and
readn(). The buffer passed in to
write() must be at least as big as the amount
of data to be written (in bytes), or a memory access error can
occur.
Always make sure that the buffer size matches the amount of data to be read or written. Buffer overflows have long been a source of security problems in software, and they can be avoided by managing memory carefully.
All the I/O classes in VPR have built-in statistics collection
capabilities. By default, the code is not activated so as to prevent
unwanted overhead. However, it can be enabled quite simply using the
method vpr::BlockIO::setIOStatStrategy().
This method takes a single parameter, a statistics collection object,
and invokes the correct methods whenever I/O occurs. Within the
specific implementation, any form of statistics related to reading and
writing of data may be collected.
From the name of the method in
vpr::BlockIO, we see the first indication that
a Strategy pattern [Gam95] is used to
implement the pluggable statistics collection code. All statistics
strategy classes must derive from
vpr::BaseIOStatsStrategy, and strategies can be mixed using the templated class
vpr::IOStatsStratgeyAdapter<S,
T>. Currently, the only strategy class is
vpr::BandwidthIOStatsStrategy, used for
collecting information about bandwidth usage of a given I/O
object.
Table of Contents
Socket programming can be a very difficult task, and the API used to write network code is difficult to understand in and of itself. The purpose of the VPR socket abstraction is thus two-fold: it abstracts the platform-specific API, and it aims to simplify the interface so that developers can focus on protocol implementations.
Readers not familiar with socket programming should consult a reference manual ([Ste98] is recommended). We do not attempt to explain the ins and outs of socket programming. Instead, we assume that readers are familiar with socket-level I/O and the ideas involved with various types of network communication.
The socket abstraction follows the concepts set forth by the
BSD sockets API,
which was also the model for the Winsock API used on Windows. In VPR,
two types of sockets may be instantiated: stream-oriented (TCP,
vpr::SocketStream) and datagram (UDP,
vpr::SocketDatagram). The helper class vpr::InetAddr
makes use of Internet Protocol (v4) addresses easier. Built on top of
vpr::SocketStream are two classes that make
writing client/server code easier:
vpr::SocketConnector and vpr::SocketAcceptor. Finally, VPR provides cross-platform data conversion
functions (see Chapter 4, Data Marshaling) to deal with
endian issues.
We begin our discussion by diving right into the common features
of sockets, as collected in the class
vpr::Socket. We assume that readers already have an understanding of
the buffered I/O concepts (see Chapter 1, Buffered I/O)
used in VPR I/O programming. The following sections cover
datagram-oriented sockets and stream-oriented sockets respectively. We
will conclude this chapter with a review of the high-level patterns
implemented for simplifying the authoring of client/server
architectures.
All socket code written using the VPR socket abstraction must use Internet Protocol (IP) addresses. The class vpr::InetAddr neatly abstracts the low-level details of using Internet addresses[1]. This class encapsulates both the IP address and the port number. It manages all the endian issues and the lookup of host names as necessary.
When constructed, a new vpr::InetAddr is
initialized to the constant value
vpr::InetAddr::AnyAddr. This value corresponds with the OS-level constant
INADDR_ANY. Typically, either a host name, a port
number, or both must be set after the object is constructed. Such
details will vary depending on the application needs. The IP address
can be set using a symbolic host name (which will be resolved through
DNS queries) or using the human-readable “dotted-decimal”
notation. The port number is set using the native byte ordering; it is
converted internally to network byte order. It is also possible to set
the host name and port number together in a single string that uses
the format “host:port”. This format is convenient when
the values for the host name and port come in as string values.
At the lowest level, all sockets have several things in common. For example, all sockets must be opened before they are used, and they must be closed when communication is complete. During communication, data is read from and written to a socket, and reads and writes may be blocking (synchronous) or non-blocking (asynchronous). All sockets are bound to a local address, and connected sockets have a remote address[2].
It is important to note that a socket does not have to be stream-oriented to be in a connected state. A datagram-oriented socket may be “connected” to a remote address so that it has a default destination. This alleviates the need to specify the destination address at every send.
These commonalities are collected into the class
vpr::Socket, which serves as the base interface for datagram- and
stream-oriented sockets. The API for this class includes methods such
as open(), close(),
send(), recv(), and
connect(). Note that
recv() and send()
are provided as analogues to read() and
write() respectively. These are included
because the BSD sockets API defines the system calls
recv(2) and send(2), in
addition to read(2) and
write(2), for use with socket file descriptors.
The extra methods are thus provided to give programmers already
familiar with the BSD sockets API an easily recognizable
interface.
Instances of vpr::Socket cannot be
created because the constructors are not public. Instances of the
concrete types vpr::SocketDatagram and
vpr::SocketStream can be used polymorphically as instances of
vpr::Socket (and
vpr::BlockIO, of course). Because the basic
operations such as read() and
write() are defined by the base class,
using the concrete socket types polymorphically could be a
convenient mechanism for mixing socket communication protocols in an
application.
Due to the semantics of sockets on Windows NT, the VPR socket
abstraction contains a slight variation of the behavior that is
available on UNIX-based systems. In Windows, once a call to
read(), write(),
accept(), etc., is made, the blocking state
of the socket is fixed[3]. That is, if the socket is a blocking socket, it will
forever remain in a blocking socket after one of these calls. The
same is true for non-blocking sockets. Furthermore, for a
stream-oriented socket that is accepting connections, the sockets
created as clients connect inherit the blocking state of the
accepting socket. The full list of methods that fix the blocking
state is as follows:
vpr::Socket::read(),
vpr::Socket::readn(),
vpr::Socket::recv(),
vpr::Socket::recvn(),
vpr::SocketDatagram::recvfrom()
vpr::Socket::write(),
vpr::Socket::send(),
vpr::SocketDatagram::sendto()
vpr::SocketStream::accept()
vpr::SocketStream::connect()
The NSPR documentation has a more complete description of this issue. We must implement our socket abstraction in this way in order to provide consistent semantics (not just consistent syntax) across platforms.
The class vpr::SocketDatagram provides VPR's abstraction to datagram-oriented
sockets, typically known as UDP (user datagram protocol) sockets.
Indeed, this class wraps the underlying operating system's
implementation of UDP sockets. The interface for
vpr::SocketDatagram extends
vpr::Socket to include the methods
sendto() and
recvfrom(), overloaded in the same way as
read() and write().
As with the operating system API, these methods are used to send a
message to a specific destination address or to receive a message from
a specific remote address, respectively.
The class vpr::SocketStream wraps the use of TCP (transmission control protocol)
sockets. TCP sockets are also known more abstractly as stream-oriented
sockets. All such sockets must be connected to a specific peer, and
thus there is no interface comparable to
vpr::SocketDatagram::sendto() or
vpr::SocketDatagram::recvfrom().
In order for connections to be made, a socket must be listening
for incoming connection requests. For that purpose, vpr::SocketStream
introduces the methods listen() (to put a
socket into a listening state) and accept()
(for accepting new connections). These work the same way as the system
calls after which they are named. However,
accept() is somewhat unique in that it takes
an unopened vpr::SocketStream object as a
parameter. The object reference is “set up” when a
successful connection occurs. Thus, when
vpr::SocketStream::accept() returns
successfully, the caller can be certain that the
vpr::SocketStream reference passed in is now a
valid, connected socket.
Finally, since stream-oriented sockets always have an accepting
socket that handles incoming connection requests,
vpr::SocketStream provides a convenience method
called openServer(). This can be used in
place of the usual open-bind-listen sequence of calls for setting up
an accepting (server) socket. Use of this method is not required for
putting a socket into a listening state; rather, it exists to shorten
user code slightly. The drawback of using it is that, in the case of
failure, the returned status will not tell the caller what stage of
setting up the listening socket failed.
Building on the foundation of stream-oriented, connected
sockets, VPR implements the Acceptor/Connector Pattern [Sch00]. The classes used in the
implementation are
vpr::SocketAcceptor and
vpr::SocketConnector. This pattern captures the concepts used in writing
stream-oriented network software. The software may use a client/server
protocol or a peer-to-peer protocol, but in either case, an initial
connection must be made to an accepting socket.
The acceptor is created using a
vpr::InetAddr object that specifies the
address on which the acceptor listens for incoming connection
requests. Once opened, the acceptor is ready to accept new
connections. The call to
vpr::SocketAcceptor::accept() uses the same
arguments and behavior as
vpr::SocketStream::accept(), so programmers
already familiar with setting up an accepting socket with
vpr::SocketStream will find
vpr::SocketAcceptor very easy to use.
The connector is designed to make non-blocking connections
easy to manage. Depending on the arguments passed to
vpr::SocketConnector::connect(), a socket
may be put into non-blocking mode if it is not already set as such.
Thus, a connection can be made “in the background” if
necessary. However, due to the semantics described in the section called “Fixed Blocking State”, after a background
connection is made, the socket must remain in non-blocking mode for
the duration of its lifetime.
[1] The current implementation of
vpr::InetAddr only supports IPv4, though
support for IPv6 will be added when the need arises.
[2] Unconnected sockets may send data to a different destination at every write. They may also receive data from any remote address.
[3] UNIX-based systems allow the blocking state to be changed from blocking to non-blocking or vice versa at any time.
Table of Contents
Most input devices used for virtual reality systems today make use of a computer's serial port for data communication. In our experience, serial port programming is not much different than other I/O programming. Implementing the communication protocol used by a given device tends to be the hard part, and that will likely be the case regardless of the underlying hardware.
The VPR serial port abstraction is based on the concepts implemented by the standard termios serial interface used by most modern UNIX-based operating systems [Ste92]. As such, the API allows enabling and disabling of a subset of the serial device features that can be manipulated using termios directly. To provide cross-platform semantics, however, some termios features are not included because there is no corresponding capability with Win32 overlapped I/O. Furthermore, any termios settings that relate only to modems are not included in the VPR serial port abstraction.
In termios, serial ports are configured by setting or clearing a wide variety of bits in various data structures. Based on this, the VPR serial port API includes methods for enabling a feature, disabling a feature, and testing the current status of a feature. For example, the following methods deal with the hardware flow control bit:
enableHardwareFlowControl():
Enables hardware flow control (if it was not already
enabled)
disableHardwareFlowControl():
Disables hardware flow control (if it was not already
disabled)
getHardwareFlowControlState():
Returns the current state of hardware flow control (true for on,
false for off)
When changing the enabled state of a serial port feature, the
change may not take effect immediately. This is determined by the
update action setting, which is manipulated by
vpr::SerialPort::setUpdateAction(). There are
three possible states (corresponding to the enumerated type
vpr::SerialTypes::UpdateActionOption):
Now: Perform the change immediately
Drain: Perform the change after all output is transmitted
Flush: Perform the change after all output is transmitted and discard all unread input
The right setting to use may depend on the specific hardware or on the desired behavior.
The serial port abstraction is handled differently than the other I/O abstraction components. We wrap two serial port interfaces: termios and Win32 overlapped I/O[4]. Because NSPR does not provide a serial port layer, we have to allow the termios to be used with NSPR on UNIX-based platforms. While this makes the implementation a little clumsy and the build system a little more complicated, it has little if any impact on users. The point of the abstraction is to hide the low-level details to provide a consistent interface across platforms.
Table of Contents
Network communication involves the transfer of data between computers, and for it to work, the two computers must be able to talk to each other using the same language. This must occur even if the two have different internal representations of the data they hold. Thus, the data must be marshaled into a common format when it is sent out and demarshaled into the local native format when it is received. VPR provides some helper functions and utility classes to simplify the efforts of network programmers.
A very common data marshaling activity is the conversion of a
multi-byte data unit from host byte order to network byte order. Such
conversions are necessary for elements of data that occupy 16 or more
bits. In VPR terms, that means the types vpr::Int16,
vpr::int32, vpr::Int64, and the unsigned
variants thereof. The interface
vpr::System provides conversion functions from host to network byte
order and vice versa for all of these types. All the functions operate
in terms of the unsigned version of the aforementioned integer types,
but they work with the signed versions as well since they simply
manipulate the actual bits. The full list of functions is as
follows:
vpr::System::Htons(): Converts a
16-bit integer from host to network byte order.
vpr::System::Ntohs(): Converts a
16-bit integer from network to host byte order.
vpr::System::Htonl(): Converts a
32-bit integer from host to network byte order.
vpr::System::Ntohl(): Converts a
32-bit integer from network to host byte order.
vpr::System::Htonll(): Converts a
64-bit integer from host to network byte order.
vpr::System::Ntohll(): Converts a
64-bit integer from network to host byte order.
Single-precision floating-point values (which occupy 32 bits of
memory) can be converted using
vpr::System::Htonl() and
vpr::System::Ntohl(). Similarly,
double-precision floating-point values (which occupy 64 bits of
memory) can be converted using
vpr::System::Htonll() and
vpr::System::Nothll().
Programmers already familiar with the operating system-level
calls such as ntohs(3) and
htonl(3) may wonder why the above functions are
named with a capital letter (i.e.,
vpr::System::Htonl() versus
vpr::System::htonl()). We have used this
naming convention because the byte order conversion functions are
preprocessor macros on some platforms, and the C preprocessor cannot
tell the difference between a method declaration and the use of a
macro. In other words, the code would not compile on platforms where
the functions are really macros.
Serializing objects is more complicated than dealing with
individual integer variables, but ultimately, a class is composed of
other data types. If the internal data types can be serialized, then
the object that holds them can be serialized as well. To enable this
functionality, VPR defines the interface
vpr::SerializableObject. It operates in terms of two other interfaces:
vpr::ObjectReader and vpr::ObjectWriter. Together, these allow an object and all the data it
aggregates to be serialized into an array of bytes that can be sent
over the network. Once received, the array can be de-serialized into a
duplicate of the original object.
The basic idea behind the object serialization interface in VPR
is the same as in Java (see the API documentation on
java.io.Serializable). An class identifies
itself as being serializable by adding
vpr::SerializableObject to its list of parent
classes. Two pure virtual methods must then be implemented:
readObject() and
writeObject(). When a class instance must be
serialized, writeObject() is invoked with an
argument that provides the class with a
vpr::ObjectWriter instance. The implementation
of writeObject() would then add the instance
data to the object writer and return. De-serializing an object occurs
in readObject() using an instance of
vpr::ObjectReader. A full class hierarchy can
be serialized and de-serialized through polymorphism. The derived
classes must simply call the parent class'
writeObject() and
readObject() methods, thus following the
class hierarchy up to the first class that identified itself as
serializable.
Because vpr::ObjectReader and vpr::ObjectWriter are abstract
types, the actual implementation of these may vary. This is similar to
the way that Java can serialize an object to a variety of data
streams. Currently, VPR can serialize a class to an array of bytes
(vpr::BufferObjectReader and
vpr::BufferObjectWriter) or to XML
(vpr::XMLObjectReader and
vpr::XMLObjectWriter). The array of bytes is suitable for network
transmission and makes sharing of classes between hosts easy.
In this part, we present the capabilities VPR provides for writing
cross-platform multi-threaded software. It is assumed that readers
already know the basics of multi-threaded programming including the
definition of thread of control. What is described here is how to use the VPR thread
interface, vpr::Thread, not how to write
multi-threaded software. For that reason, it is recommended that readers
be familiar with the following publications before continuing:
Pthreads Programming by Bradford Nichols, Dick Buttlar, and Jacqueline Proulx Farrell.
The sproc(2) manual page on IRIX or on
SGI's
technical publications site.
The pthread(3) manual page for your
operating system. The pthread functions are part of a POSIX standard
and will be the same across platforms.
Table of Contents
When considering multi-threaded programming, it is important to know that with great power comes great responsibility. The power is being able to provide multiple threads of control in a single application. The responsibility is making sure those threads get along with each other and do not step on each other's data. VR Juggler is a multi-threaded library which makes it very powerful and very complex.
As a cross-platform framework, VR Juggler uses an internal threading abstraction that provides a uniform interface to platform-specific threading implementations. That cross-platform interface is available to programmers to make applications multi-threaded without tying them to a specific operating system's threading implementation.
The threading interface in VPR is modeled after the POSIX thread
specification of POSIX.1b (formerly POSIX.4). The main difference is
that VPR's interface is object-oriented while POSIX threads (pthreads)
are procedural. The basic principles are exactly the same, however. A
function (or class method) is provided to the
vpr::Thread class, and that function is
executed in a thread of control that is independent of the creating
thread.
Threads are spawned (initialized and begin execution) when the
vpr::Thread constructor is called. That is,
when instantiating a vpr::Thread object, a new
thread of execution is created. The semantics of threads says that a
thread can begin execution at any time after being created, and this
is true with vpr::Threads. Do not make any
assumptions about when the thread will begin running. It may happen
before or after the constructor returns the
vpr::Thread object.
To pass arguments to threads, the common mechanism of
encapsulating them in a C++ struct must be used. The
function executed by the thread takes only a single argument of type
void*. An argument is not required, of course, but to
pass more than one argument to a thread, the best way to do this is to
create a structure and pass a pointer to it to the
vpr::Thread constructor.
Once a vpr::Thread object is created, it
acts as an interface into controlling the thread it encapsulates.
Thread signals can be sent, priority changes can be made, execution
can be suspended, etc. This interface is the focus of this
section.
We begin our discussion of creating threads with VPR by
explaining the use of the class vpr::Thread.
Use of vpr::Thread is intended to be easy.
Multi-threaded programming has enough complications without having a
difficult API as well. In almost all cases, thread creation can be
done in a single step, executed one of two ways:
The second appears easier, but to create the functor, an
argument to the function executed by the thread may still have to be
passed. The presence of the argument depends on the specific function
being run by the thread. In addition to the function pointer or
functor, parameters such as the priority and the stack size may be
passed to the vpr::Thread constructor, but the
defaults for the constructor are quite reasonable.
A minor issue with creating a vpr::Thread
is the concept of functors. The topic of functors will be put off
until the next section. For now, just think of them as wrappers around
function pointers.
Before writing code that uses
vpr::Threads, make sure that the header file
vpr/Thread/Thread.h is included. Never include
the platform-specific headers such as
vpr/md/POSIX/Thread/ThreadPosix.h. The single
file vpr/Thread/Thread.h is all that is
required.
The following example illustrates how to create a thread that
will execute a function called run() that takes
no arguments. The prototype for run()
is:
void run(void* args);
This will be the same across all platforms. The thread creation code is then:
vpr::Thread* thread; thread = new vpr::Thread(run);
At this point, a newly spawned thread is executing the code in
run(). It is advisable to hang onto the
variable thread so that the thread may be
controlled as necessary.
That was pretty easy. What if you want to pass one or more
arguments to run() so that its behavior can be
modified based on some variables? Not surprisingly, that is fairly
easy too. As mentioned above, if there is more than one argument to
pass to the thread function, they will have to be collected into a
struct, and pointer to that struct will have to be
passed. A common way to do this is as follows:
struct ThreadArgs
{
int id;
char name[40];
// And so on...
};
void someFunc()
{
// Other code ...
ThreadArgs* args;
vpr::Thread* thread;
args = new ThreadArgs();
// Fill in the elements of args ...
thread = new vpr::Thread(run, (void*) args);
}When creating a single thread, this works beautifully. If multiple threads are needed, all taking the same type of argument, there must be a separate argument structure instance for each one. A bunch of pointers can be declared, or the same pointer can be reused over and over. The address passed to each thread will be unique either way. Using this method requires that the argument memory be released before the thread exits, of course.
Once we have a thread running, it is often useful to synchronize another thread so that its execution halts until the running thread has completed. This is called “joining threads”. The following example illustrates how this can be done:
vpr::Thread* thread; thread = new vpr::Thread(run); // Do other things while the thread is going ... thread->join(); // Now that the thread is done, continue.
Here, the creator of thread can be another
vpr::Thread, or it can be the main thread of
execution. In other words, any thread can create more threads and
control them. What happens in this example is that thread is created
and begins running. Meanwhile, the creator thread continues to do
some more work and then must wait for thread to
finish its work before continuing. It calls the
join() method, a blocking call, and it will
not return until thread has completed.
While it is not demonstrated here, the
join() method can take a single argument of
type void**. It is a pointer to a pointer where the
exit status of the joined thread is stored. The operating system
fills the pointed to pointer with the exit status when the thread
exits.
Sometimes, it may be necessary to suspend the execution of a
running thread and resume it again later. There are two methods in
the vpr::Thread interface that do just this.
Assuming that there is already a running thread pointed to by the
object thread, it can be suspended as
follows:
thread->suspend();
Resuming execution of the suspended thread is just as easy:
thread->resume();
On successful completion, both methods return
vpr::ReturnStatus::Succeed. If the operation
could not be performed for some reason,
vpr::ReturnStatus::Fail is returned to indicate
error status.
Changing the priority of a thread tells the underlying
operating system how important a thread is and gives it hints about
how to schedule the threads. If no value for the priority is given
to the constructor, all vpr::Threads are
created with the default priority for all threads. Values higher
than 0 for the priority request a higher priority when the thread is
created.
Besides being able to set the priority when the thread is
created, it is possible to query and to adjust the priority of a
running thread. Assuming that there is already a running thread
pointed to by the object thread, its priority can
be requested as follows:
int prio; thread->getPrio(&prio);
The thread's priority is stored in prio and
returned via the pointer passed to the
getPrio() method. Setting that thread's
priority is also easy:
int prio; // Assign some priority value to prio ... thread->setPrio(prio);
On successful completion, both methods return
vpr::ReturnStatus::Succeed. If the operation
could not be performed for some reason,
vpr::ReturnStatus::Fail is returned to indicate
error status.
On UNIX-based systems, a signal is sent to a process using the
kill(2) system call. With POSIX threads,
signals are sent using pthread_kill(3). VPR's
thread interface implements these ideas using a
kill() method.
There are two ways to call this method: with an argument naming the
signal to be delivered to the thread or without an argument which
cancels the thread's execution. The first of these is described in
this section, and the second is described in the next
section.
A problem does arise here, unfortunately. Signals are not
supported on all operating systems (notably, Win32). The interface
is consistent, but code written on IRIX will not compile on Win32
if, for example, it sends a SIGHUP to a thread.
An improved thread interface is being designed to overcome problems
such as this one. For now, we describe this part of the interface as
though it is supported completely on all platforms.
As usual, assume there is a running thread, a pointer to which
is stored in thread. To send it a signal
(SIGINT, for example), use the
following:
thread->kill(SIGINT);
The signal will be delivered to the thread by the operating
system, and the thread is expected to handle it properly. This
version of the kill() method returns
vpr::ReturnStatus::Succeed if the signal is
sent successfully. Otherwise,
vpr::ReturnStatus::Fail is returned to indicate
that an error occurred.
As described in the previous section, using the
kill() method with no argument cancels the
execution of the thread. When using POSIX threads, this is actually
implemented using pthread_cancel(3). On
IRIX with SPROC threads, a SIGKILL is sent to
the thread to end its execution forcibly. The syntax for using this
method is basically the same as in the previous section, but it is
repeated to make that clear. Again assuming that there is a running
thread with a pointer to its vpr::Thread
object stored in thread, use the
following:
thread->kill();
Unlike the syntax used to send a signal to a thread, this
version of kill() does not have a return
value.
Users of POSIX threads may be wondering if the
vpr::Thread API provides a way to set
cancellation points in the code. Unfortunately, it does not at this
time. Extending the interface in this way is being considered, but
cancellation points do not have meaning with all thread
implementations.
Lastly, it is common to request the currently running thread's
identifier. This only makes sense when called from a point on that
thread's flow of execution. (In POSIX threads, this is the notion of
“self”. For IRIX SPROC threads, this means getting the
process ID.) The vpr::Thread API provides a
static method that can be called at any time in the thread that is
currently running. It returns a pointer to a
vpr::BaseThread (the basic type from which
vpr::Thread inherits its interface). The
syntax is as follows:
vpr::BaseThread* my_id; my_id = vpr::Thread::self();
The returned pointer can then be used to perform all of the previously described operations on the current thread.
The current threading implementation in VPR is a little
difficult to understand. The code is not complicated at all, but
because all platform-specific implementations are referred to as
vpr::Threads, the details can get lost in the
shuffle. To begin, the current list of platform-specific thread
implementation wrapper classes are:
vpr::ThreadSGI: A wrapper around
IRIX SPROC threads (refer to the sproc(2)
manual page for more information)
vpr::ThreadPosix: A wrapper around
POSIX threads (both Draft 4 and Draft 10 of the standard are
supported)
vpr::ThreadNSPR: A wrapper around
Netscape Portable Runtime threads
The interface itself is defined in
vpr::BaseThread, and all of the above classes
inherit from that class.
The threading implementation used is chosen when VPR is
compiled. To use a certain type of thread system, be sure that the
version of VPR in use was compiled with the type of threads desired.
When the VPR build is configured, preprocessor
#define statements are made in
vpr/vprDefines.h that describe the threading
system to use. Based on that, the header file
vpr/Thread/Thread.h makes several
typedefs that set up one of the platform-specific
thread implementations to act as the
vpr::Thread interface. For example, if
compiling on Win32, the class vpr::ThreadNSPR
is typedef'd to be vpr::Thread.
Since the interface is consistent among all the wrappers, everything
works as though that was the way it was written to behave.
The current implementation is modeled after the POSIX thread API for the most part. When designing it, we approached it with the idea that having a more complete API was more important than having a “lowest-common-denominator” API. That is, just because not all threading implementations support a specific feature does not mean that the API should suffer by not having that feature. Whether this was a good approach or not is an open debate.
VPR has a wrapper around Netscape Portable Runtime (NSPR) threads. We have removed the Win32-specific threads because NSPR already supports that implementation. Further implementations may be removed in favor of using what NSPR offers. Doing this will offload much of our efforts onto the NSPR. NSPR threads do not support all the features we have, however, because they took the lowest-common-denominator approach. As with all technology, there is a trade-off in relieving some of our work load by using an existing cross-platform thread implementation: our interface becomes limited to what features that implementation provides. It remains to be seen exactly how much of VPR's threading subsystem will be removed, and those programmers who choose to use it should be careful to watch the mailing lists for discussions and announcements about changes.
In this section, we explain the concept and use of functors. A functor is a
high-level concept that encapsulates something quite simple. A functor
is defined as “something that performs an operation or a
function.” While this is not very detailed, it is clear and
concise. In VPR functors can be used as the code executed by a thread
(refer to the section called “Threads: Using vpr::Thread” for more detail on the
topic of vpr::Threads). This section describes
how to use functors for exactly that purpose.
As mentioned, a functor is used in VPR with
vpr::Threads. VPR's threads can execute two types of functions:
normal C/C++ functions and class methods or functors. The former was
described in the section about using
vpr::Threads, and the latter is described
here. The use of functors is given more attention because the
concept may be foreign to some programmers. Those who already know
about functors can skip this short description and go straight to
the section on using functors.
In VPR, a functor is simply another object type that happens to encapsulate a user-defined function. The details on how this is done are not important here, but they are provided later for those who are interested. What is important to know is that a functor can be thought of as a normal function. When using them, programmers simply implement a function and then pass the function pointer (and the function's optional argument) to the functor's constructor. The object does the rest.
Observant readers may have noticed the parenthetical phrase in
the previous paragraph mentioning a function's optional argument.
Note that “argument” is singular meaning that only one
parameter can be passed to the function that will be run by the
created thread. The type of that argument is the wonderfully vague
void*, an artifact of basing the threading subsystem on
C libraries. As discussed in the section on using
vpr::Threads, if there is a need to pass
multiple arguments, they must be encapsulated in a struct or a
comparable object.
Once a functor object exists, it is passed to the
vpr::Thread constructor, and the new thread
will execute the functor (which knows about the function). The end
result is the same as using a normal C/C++ function or a static
class member function, but there is one special benefit: with
functors, non-static class member functions can be passed. In many
cases, there arises a need to run a member function in a separate
thread, but making it static is infeasible or awkward. Thus, it
would be best to pass a non-static member function to the created
thread. To get access to the non-static data members, however, the
C++ this pointer must be available to the thread.
By using a VPR functor, that is all handled behind the scenes so
that passing a non-static member function is straightforward.
Before getting into specifics, there is a header file that
must be included to use VPR thread functors. In this case, the
header is vpr/Thread/ThreadFunctor.h. Within
this header, vpr::BaseThreadFunctor is
declared as an abstract base class. It has two subclasses
implementing its interface:
vpr::ThreadMemberFunctor and
vpr::ThreadNonMemberFunctor. Both of these
subclasses will be discussed in turn next.
This implementation of
vpr::BaseThreadFunctor is for all functions
that fall into the rather elite category of being non-static class
member functions. To be more specific, those member functions
(heretofore referred to as “methods”) must have the
following prototype:
void methodName(void* arg);
Those readers with experience in multi-threaded programming will recognize this prototype instantly. It is no different than that used in common threading implementations. Constructing the functor to use this method, however, is quite different than what readers may have seen before.
Say there is a class MyObject with a
method run() having the appropriate
prototype that will be executed by a
vpr::Thread object. In this case, run()
takes an argument that is a pointer to a
user-defined type thread_args_t.
Also assume that there is an instance of
MyObject pointed to by the variable
my_obj. The following code creates the
vpr::ThreadMemberFunctor object that will
encapsulate the method:
vpr::ThreadMemberFunctor<MyObject>* my_functor; thread_args_t* args; vpr::Thread* thread; args = new thread_args_t(); // Fill in the arguments to be passed to the thread... my_functor = new vpr::ThreadMemberFunctor<MyObject>(my_obj, &MyObject::run, (void*) args); thread = new vpr::Thread(my_functor);
The important thing to note in this example is that
vpr::ThreadMemberFunctor is a template class.
When creating the functor instance, the class must be specified as
the template parameter. (If you do not understand this syntax, take
a look at a C++ book that covers the current C++ standard.) Also
note that when creating the new vpr::Thread
object, the argument structure is not passed to the constructor. The
argument to run() is packaged up with the
function in the functor object my_functor. Once
this code has executed, a new thread is spawned that will run the
run() method given the provided argument
structure.
The vpr::BaseThreadFunctor interface
defines an extra method setArg() that allows the function's argument to be set after
the functor object is created. The argument to the constructor
providing the function's argument is optional and will default to
NULL if not specified. At a later time, should
there be an argument to provide, the following can be used:
thread_args_t* args = new thread_args_t(); // Fill in args ... my_functor->setArg(args);
This assumes that there is already a functor object
instantiated called my_functor. Alternatively,
setArg() could be used to remove a
previously defined argument by passing
NULL.
The last thing to note is that a lot of memory is being allocated dynamically in the example code. Be careful to deallocate the memory when it is no longer needed.
An extension to the concept of
vpr::ThreadMemberFunctor has been written to
handle a situation that often arises. Typically, a class will have
data members that define the state of a class instance. Thus, when
using a member functor to create a thread, the object passed to the
vpr::ThreadMemberFunctor constructor probably
has all the state information that is required for the thread to
execute. In this case, the single void* parameter held
by the functor is ignored entirely when the thread executes.
To address this case (and to simplify user code), the class
vpr::ThreadRunFunctor exists. A class that
implements the “runnable” concept can be used to create
a thread with vpr::ThreadRunFunctor. Such a
class has a public method named run() that
takes no parameters and has a void return type. The
declaration of a runnable class would thus appear similar to the
following:
class MyClass
{
public:
void run();
// Other public methods ...
private:
// Some internal data members ...
};To create a thread using this class, the code would appear as
follows (assuming the existence of a heap-allocated variable
my_obj of type MyClass*):
vpr::ThreadRunFunctor<MyClass>* my_functor = new vpr::ThreadRunFunctor<MyClass>(my_obj); vpr::Thread* my_thread = new vpr::Thread(my_functor);
The spawned thread will execute the method
MyClass::run().
While the above code is much simpler than what was presented
in the section called “vpr::ThreadMemberFunctor”, there is
one major drawback. Namely, the run()
method must be public, and it could therefore be invoked outside the
scope of an executing thread. This would have to be the case if VPR
provided an abstract base class such as
Runnable (similar to the Java interface
Runnable) with a pure virtual method
named run(). If the visibility of the
member function executed by the thread is a concern, use
vpr::ThreadMemberFunctor.
This implementation of
vpr::BaseThreadFunctor is the complement of
the set of functions contained by
vpr::ThreadMemberFunctor. It is used for
normal C/C++ functions and for static class member functions. There
is nothing terribly interesting about this class, and its use is
straightforward. The following example, an adaptation of that
presented in the previous section, shows how to use this interface
rather than passing a function pointer and an argument to the
vpr::Thread constructor. In this case, assume that the function
run() is appropriately defined for use
here.
vpr::ThreadNonMemberFunctor* my_functor; thread_args_t* args; vpr::Thread* thread; args = new thread_args_t(); // Fill in the arguments to be passed to the thread... my_functor = new vpr::ThreadNonMemberFunctor(run, (void*) args); thread = new vpr::Thread(my_functor);
That is all there is to it. Programmers end up doing more work
than if they had just passed the function pointer and the associated
argument to the vpr::Thread constructor
directly, but the vpr::Thread constructor is
relieved of some work. (The reason for this is described below.)
Thus, either way is equally efficient, and what you use is up to
you.
The magic behind these functors is done by overloading
operator() for
vpr::BaseThreadFunctor objects. Both
implementations of the interface store the function pointer (and
optional argument pointer), and when
vpr::BaseThreadFunctor::operator() is
invoked, they call the function and pass the argument if there is
one. There is a little more magic with the
vpr::ThreadMemberFunctor, however, that
allows it to work with the non-static methods of a given
class.
The class vpr::ThreadMemberFunctor
works its extra-special magic through the use of a template and one
of C++'s dustier operators, ::*. This operator is used to point to a
member of a class. In this case, it points to the method that will
be executed by the thread. When used in conjunction with the
provided class instance (the this pointer), the
non-static method can be invoked by the functor.
One interesting thing to note about
vpr::Threads is that they deal only in
functors. More specifically, they deal only with objects that
subsume to vpr::BaseThreadFunctor. If a
function pointer and its argument are passed directly to the
vpr::Thread constructor, a
vpr::ThreadNonMemberFunctor object is created
to package those arguments. That new functor is then used internally
by the thread. Thus, whether you choose to create a non-member
functor or to pass the function pointer and associated argument, the
same code will be executed.
Table of Contents
When multiple processes or threads have access to the same data, synchronization of reads and writes becomes an important concern. For example, if one thread writes to a shared variable when another thread is reading, the value read will be corrupted. If two threads try to write to the same shared variable at the same time, one of the two writes will be lost. These situations can lead to unexpected, and often undesirable, program execution. For that reason, it is important to understand how to protect access to shared data so that the multi-threaded software will execute correctly.
The most important part of multi-threaded programming is proper thread synchronization so that access to shared data is controlled. Doing so results in consistency among all threads. Semaphores are a very common synchronization mechanism and have been used widely in concurrent systems. This section describes the cross-platform semaphore interface provided with and used by VPR. It does not explain what semaphores are or how to use them—it is assumed that readers are already familiar with the topic lest they probably would not be reading this chapter at all.
As with threads, a cross-platform abstraction layer has been written to provide a consistent way to use semaphores on all supported platforms. The primary goal behind the interface design is to provide the common P (acquire) and V (release) operations. The interface does include methods for read/write semaphores, but as of this writing, that part of the interface is not complete. Because of that, the use section does not cover that part of the interface. When the implementation is complete, this section will be expanded.
As always, there is a header file that must be included to use
vpr::Semaphore. This time around, the file is
vpr/Sync/Semaphore.h. Do not include any of the
platform-specific implementation files. That is all handled
appropriately within
vpr/Sync/Semaphore.h.
When creating a vpr::Semaphore object,
give the initial value that represents the number of resources being
controlled by the semaphore. If no value is given, the default is 1
which of course gives a binary semaphore. Binary semaphores are
better known as mutexes (see the section called “Mutual Exclusion: Using
vpr::Mutex” for more information about mutex
use in VPR). An example of creating a simple semaphore to control
access to five resources is as follows:
vpr::Semaphore sema(5);
This creates a semaphore capable of controlling concurrent
access to five resources. At some point, if there is a need to
change the number of resources, a method called
reset() is provided. Pass the new number of resources, and
the semaphore object is updated appropriately:
sema.reset(4);
The semaphore sema now controls access to
only four resources.
When a thread needs to acquire access to shared data, it locks
a semaphore. In the vpr::Semaphore interface,
this is accomplished using the acquire()
method:
sema.acquire();
As expected, acquire() is a blocking
call, so if the semaphore's value is less than or equal to 0, the
thread requesting the lock will block until the semaphore's value is
greater than 0. If the lock is acquired,
vpr::ReturnStatus::Succeed is returned. If the
attempt to lock the semaphore fails for some reason,
vpr::ReturnStatus::Fail is returned.
Finally, when access to the critical section is complete, the
semaphore is released using the release()
method:
sema.release();
If the locked semaphore is released successfully,
vpr::ReturnStatus::Succeed is returned.
Otherwise, vpr::ReturnStatus::Fail is
returned.
Those who have read the Gory Details section for
vpr::Threads will find this section very
familiar. As with vpr::Threads, there are
several platform-specific semaphore implementation wrapper
classes:
vpr::SemaphoreSGI: A wrapper around
IRIX shared-arena semaphores (refer to the
usnewsema(3P) and related manual pages for
more information)
vpr::SemaphorePosix: A wrapper
around POSIX real-time semaphores (POSIX.1b, formerly
POSIX.4)
vpr::SemaphoreNSPR: An
implementation of semaphores using NSPR primitives
Unlike vpr::Thread, however, there is
no base interface from which these implementations inherit.
Performance decreases caused by virtual functions are avoided this
way.
The semaphore implementation used is chosen when VPR is
compiled and will always match the thread implementation being used.
When the VPR build is configured, preprocessor
#define statements are made in
vpr/vprDefines.h that describe the threading
system and thus the semaphores to use. Based on that, the header
file vpr/Sync/Semaphore.h makes several
typedefs that set up one of the platform-specific
implementations to act as the vpr::Semaphore
interface. For example, if compiling on Linux, the class
vpr::SempahorePosix is typedef'd
to vpr::Semaphore. Since the interface is
consistent among all the wrappers, everything works as though that
was the way it was written to behave.
In addition to cross-platform semaphores, VPR provides an
abstraction for cross-platform mutexes. Mutexes are a special type of
semaphore known as a binary semaphore. Exactly one thread can hold the
lock at any time. This very short section, however, is not about
mutexes but rather about the vpr::Mutex
interface provided with and used by VPR.
The cross-platform mutex abstraction in VPR is critical for
synchronizing access to shared data. Those who have read the section
on vpr::Semaphore will find this section very, very familiar. The
interface for vpr::Mutex is a subset of that
for vpr::Semaphore since mutexes are binary
semaphores. They can be locked and unlocked. That is all there is to
know. The vpr::Mutex interface does include
some methods for read/write mutexes, but this implementation is
incomplete and is not documented here for that reason. When the
implementation is finished, this documentation will be
expanded.
The header file to include for using
vpr::Mutex is
vpr/Sync/Mutex.h. As with other classes
discussed in this chapter, it is important not to include the
platform-specific header files.
When creating a vpr::Mutex object,
there are no special parameters to pass or considerations to be
made. An example of creating a mutex is as follows:
vpr::Mutex mutex;
There is nothing more to say this time.
When a thread needs to acquire access to shared data, it can
lock a mutex. In the vpr::Mutex interface,
this is accomplished using the acquire()
method:
mutex.acquire();
As expected, acquire() is a blocking
call, so if the mutex is already locked by another thread, the
thread requesting the lock will block until the mutex is released by
the other thread. If the lock is acquired,
vpr::ReturnStatus::Succeed is returned. If the
attempt to lock the semaphore fails for some reason,
vpr::ReturnStatus::Fail is returned.
If there is a need to lock a mutex only when the call would
not block, a method is provided to do this. It
is called tryAcquire(), and it will not
block if the mutex is already locked. It works as follows:
mutex.tryAcquire();
If the mutex is locked,
vpr::ReturnStatus::Succeed is returned.
Otherwise, vpr::ReturnStatus::Fail is returned.
The call does not block.
In addition to conditional locking, the state of a mutex can
be tested to see if it is locked or unlocked. This is done using the
test() method as follows:
int state = mutex.test();
If the mutex is not locked,
vpr::ReturnStatus::Fail is returned. Otherwise,
vpr::ReturnStatus::Succeed is returned.
When access to the critical section is complete, a locked
mutex is released using the release()
method:
mutex.release();
If the locked mutex is released successfully,
vpr::ReturnStatus::Succeed is returned.
Otherwise, vpr::ReturnStatus::Fail is
returned.
Those who have read the Gory Details sections for
vpr::Threads or for
vpr::Semaphores will find this last section
very familiar (and probably uninteresting at this point). As with
vpr::Threads and
vpr::Semaphores, there are several
platform-specific mutex implementation wrapper classes:
vpr::MutexSGI: A wrapper around
IRIX shared-arena mutexes (refer to the
usnewlock(3P) and related manual pages for
more information)
vpr::MutexPosix: A wrapper around
POSIX real-time mutexes (POSIX.1b, formerly POSIX.4)
vpr::MutexNSPR: A wrapper around
NSPR mutexes
Similar to vpr::Semaphore, there is no
base interface from which these implementations inherit. Performance
issues caused by virtual functions are avoided by doing this.
The mutex implementation used is chosen when VPR is compiled
and will always match the thread implementation being used. When the
VPR build is configured, preprocessor #define
statements are made in vpr/vprDefines.h that
describe the threading system and thus the mutexes to use. Based on
that, the header file vpr/Sync/Mutex.h makes
several typedefs that set up one of the
platform-specific implementations to act as the
vpr::Mutex interface. For example, if
compiling on Solaris, the class
vpr::MutexPosix is typedef'd to
be vpr::Mutex. Since the interface is
consistent among all the wrappers, everything works as though that
was the way it was written to behave.
Condition variables are a helpful extension to mutexes. Every condition variable has an associated mutex, and thus they can be used to control mutually exclusive access to some resource. A condition variable adds the ability to test a condition and wait for its state to change in some meaningful way. Waiting threads are awakened when the state of the variable changes or when a time interval expires. When awoken, the relevant condition is tested. If the state has changed to the desired result, the thread continues its execution. If not, it goes back into the waiting state until it is awakened again to repeat the process.
The interface for
vpr::CondVar is very similar to that of
vpr::Mutex. This is because every instance of
vpr::CondVar contains an instance of
vpr::Mutex. Thus, acquiring and releasing a
condition variable actually acquires and releases the contained
mutex.
The header file to include for using
vpr::CondVar is
vpr/Sync/CondVar.h. As with other classes
discussed in this chapter, it is important not to include the
platform-specific header files.
When creating a vpr::CondVar object,
there are no special parameters to pass or considerations to be
made. An example of creating a condition variable is as
follows:
vpr::CondVar cv;
In addition to the vpr::CondVar
instance, there is usually some associated variable whose value will
be tested and modified by various threads. For our purposes, we will
use a boolean variable:
bool state_var;
When a thread needs to acquire access to shared data, it can
lock a condition variable. In the
vpr::CondVar interface, this is accomplished
using the acquire() method:
cv.acquire();
As expected, acquire() is a blocking
call, so if the condition variable's mutex is already locked by
another thread, the thread requesting the lock will block until the
mutex is released by the other thread. If the lock is acquired,
vpr::ReturnStatus::Succeed is returned. If the
attempt to lock the semaphore fails for some reason,
vpr::ReturnStatus::Fail is returned.
When access to the critical section is complete, a locked
condition variable is released using the
release() method:
cv.release();
If the locked condition variable is released successfully,
vpr::ReturnStatus::Succeed is returned.
Otherwise, vpr::ReturnStatus::Fail is
returned.
Prior to calling
vpr::CondVar::release(), it is almost
always necessary to call
vpr::CondVar::signal() (or
vpr::CondVar::broadcast()) to inform any
waiting threads that the condition on which they are waiting may
have changed. The next section describes this procedure in greater
detail.
So far, we have seen how to lock and unlock a condition
variable, but we have not seen how to use a
condition variable. Let us say that we have two threads running
concurrently. One thread is performing an operation, and the other
thread must wait until that operation is complete. Condition
variables work wonderfully in such a situation. For the following
description, we will call the first thread the worker thread and the
second thread the waiting thread. For the example, we will use the
condition variable cv and the boolean flag
state_var, introduced above. Initially,
state_var will be set to false
to indicate that the worker thread has not done its job yet.
While it may seem more logical to focus on the worker thread
first, our example will flow better if we start with the waiting
thread. That way, the waiting thread can be in its waiting state
(at least conceptually) when we talk about the worker thread. The
waiting thread will of course be waiting for the condition (the
value of state_var) to change.
In order for the waiting thread to be waiting, it must have
tested the value of state_var. Because
state_var is shared data, the condition
variable must be locked before it can read the current value of
state_var. Depending on the current value, the
waiting thread will either wait or continue its execution. The
usual process for doing this is shown below:
cv.acquire();
{
while ( state_var == false )
{
cv.wait();
}
}
cv.release();The call to vpr::CondVar::wait() is
special: the mutex associated with cv is
unlocked so that other threads can manipulate
state_var, but the waiting thread will block.
When the waiting thread is awakened, it will regain the lock it
previously held on the condition variable. The thread will test
state_var again, and if its value is now
true, the waiting thread will exit the loop and
release the condition variable. If the value of
state_var is still false,
the waiting thread will go back to its waiting state.
The worker thread has some task to perform that is critical
to the proper execution of the two active threads. In order to
inform the waiting thread about the current status of the task, we
will use the condition variable cv and the
boolean flag state_var, shown above. While the
worker thread is performing its task, it must hold the lock on the
condition variable, as shown below:
cv.acquire();
{
// Perform our critical task ...
state_var = true;
cv.signal();
}
cv.release();Once the “critical task” is complete, the value
of state_var is changed to
true to indicate that the job is done. Then,
vpr::CondVar::signal() is invoked. This
will wake up the waiting thread, and the condition variable is
released. The result is that the waiting thread will wake up to
discover that the job has been completed, and it will continue
with its execution.
The method vpr::CondVar::signal()
will wake up at most one waiting thread. Which thread is
awakened is determined by the operating system scheduling
algorithms. To wake up all threads, use
vpr::CondVar::broadcast(). Of course,
each waiting thread will have to wait its turn to get exclusive
access to test the condition, but this can be useful when it is
known that many threads are all waiting on the same
result.
VPR contains an abstraction for allowing cross-platform signal handling. The interface is based on that used in the ADAPTIVE Communication Environment (ACE). The basic idea is that a set of signals is associated with a signal handler. The handler is registered with the operating system, and whenever one of the signals in the signal set is delivered to the proces