VR Juggler Portable Runtime

Programmer's Guide

Verrsion 2.0

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: 2007-06-29 17:38:02 -0500 (Fri, 29 Jun 2007) $


Table of Contents

Vaporous Programming
I. Input/Output
1. Buffered I/O
Opening and Closing
Setting Attributes for Opening
Blocking Versus Non-Blocking
Reading and Writing
Statistics Collection
2. Sockets
Internet Addresses
Socket Commonalities
Uses of vpr::Socket
Fixed Blocking State
Datagram-Oriented Sockets
Stream-Oriented Sockets
The Acceptor/Connector Pattern
The Acceptor
The Connector
3. Serial Ports
Interface Overview
Abstraction Details
4. Data Marshaling
Endian Conversion
Object Serialization
II. Multi-Threading
5. Creating Threads
Threads: Using vpr::Thread
Creating Threads
Waiting for a Thread to Complete
Suspending and Resuming a Thread's Execution
Getting and Setting a Thread's Priority
Sending Signals to a Thread
Canceling a Thread's Execution
Requesting the Current Thread's Identifier
The Gory Details
Thread Functors
High-Level Description
Functors from (Non-Static) Member Functions
Functors from Static Member Functions
Functors from Non-Member Functions
Functors from Callable Objects
Updating from VPR 1.0
6. Synchronization
Semaphores: Using vpr::Semaphore
High-Level Description
Creating a Semaphore
Locking a Semaphore
Releasing a Locked Semaphore
The Gory Details
Mutual Exclusion: Using vpr::Mutex
High-Level Description
Creating a Mutex
Locking a Mutex
Attempting to Lock a Mutex
Testing the State of a Mutex
Releasing a Locked Mutex
The Gory Details
Condition Variables: Using vpr::CondVar
High-Level Description
Creating a Condition Variable
Locking a Condition Variable
Attempting to Lock a Condition Variable
Releasing a Locked Condition Variable
Putting Condition Variables to Use
7. Signal Handling
III. Miscellaneous Utilities
8. Simulated Sockets
Sim Socket Interface
Sim Socket Implementation
9. Globally Unique Identifiers
GUID Creation
GUID Operations
10. Intervals
Interval Creation
Interval Operations
11. Singletons
12. Factories
13. Performance Monitoring
IV. Appendices
A. I/O Implementation Information
B. GNU Free Documentation License
PREAMBLE
APPLICABILITY AND DEFINITIONS
VERBATIM COPYING
COPYING IN QUANTITY
MODIFICATIONS
COMBINING DOCUMENTS
COLLECTIONS OF DOCUMENTS
AGGREGATION WITH INDEPENDENT WORKS
TRANSLATION
TERMINATION
FUTURE REVISIONS OF THIS LICENSE
ADDENDUM: How to use this License for your documents
Bibliography
Glossary of Terms
Index

List of Examples

5.1. Using Boost.Bind to Create Thread Functors
5.2. Member Function for Thread Functor (1)
5.3. Member Function for Thread Functor (2)
5.4. Member Function for Thread Functor (3)
5.5. Member Function for Thread Functor (4)
5.6. Static Member Function for Thread Functor (1)
5.7. Static Member Function for Thread Functor (2)
5.8. Static Member Function for Thread Functor (3)
5.9. Non-Member Function for Thread Functor
5.10. Callable Object for Thread Functor (1)
5.11. Callable Object for Thread Functor (2)
5.12. Callable Object for Thread Functor (3)
5.13. VPR 1.0 Use of Thread Non-Member Functor
5.14. Updated Use of Static Member Function for Thread Functor
5.15. VPR 1.0 Use of Thread Member Functor
5.16. Updated Use of Member Function for Thread Functor
11.1. Use of vprSingletonHeader()
11.2. Use of vprSingletonImp()
11.3. Use of vprSingletonHeaderWithInitFunc()
11.4. Use of vprSingletonImpWithInitFunc()
11.5. Use of vprSingletonImpLifetime()
11.6. Use of vpr::Singleton<T>

Vaporous Programming

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, Win32 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.

Part I. Input/Output

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.

Chapter 1. Buffered I/O

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

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.

Setting Attributes for Opening

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.

Blocking Versus Non-Blocking

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.

Reading and Writing

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.

Tip

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.

Statistics Collection

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.

Chapter 2. Sockets

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.

Note

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.

Internet Addresses

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.

Socket Commonalities

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].

Note

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.

Uses of vpr::Socket

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.

Fixed Blocking State

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.

Datagram-Oriented Sockets

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.

Stream-Oriented Sockets

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.

The Acceptor/Connector Pattern

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

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

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.

Chapter 3. Serial Ports

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.

Interface Overview

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):

  1. Now: Perform the change immediately

  2. Drain: Perform the change after all output is transmitted

  3. 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.

Abstraction Details

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

Chapter 4. Data Marshaling

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.

Endian Conversion

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().

Note

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.

Object Serialization

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.

Part II. Multi-Threading

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.

Chapter 5. Creating Threads

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.

Threads: Using vpr::Thread

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:

  1. Pass a function pointer to the vpr::Thread constructor along with any argument that should be passed to the function when the thread is created

  2. Pass a functor (a callable object) to the vpr::Thread constructor

The second appears easier, but to create the functor, parameters to the function executed by the thread may still have to be passed. The presence of parameters 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 callable objects.

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.

Creating Threads

The following example illustrates how to create a thread that will execute a free function called run() that takes no arguments. The prototype for run() is:

void run();

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? One approach is to define the function so that it takes a parameter that is an aggregate type (a struct or a class). The data needed by the function is then collected into this aggregate type and packaged with the function pointer. A common way to do this is as follows:

struct ThreadArgs
{
   int id;
   std::string name;
   // And so on...
};

void run(const ThreadArgs& args)
{
   // Do work ...
}

void someFunc()
{
   // Other code ...

   ThreadArgs args;
   args.id = 50;
   args.name = "My Thread";
   // And so on ...

   vpr::Thread* thread;
   thread = new vpr::Thread(boost::bind(run, args);
}

When creating a single thread, this works beautifully. If multiple threads are needed, all taking the same type of argument, there would usually have to be a separate argument structure instance for each one. A bunch of objects can be declared, or the same objects can be reused over and over.

The preceding example made use of Boost.Bind to couple the run() function with a struct instance. Instead of declaring a struct to bundle all the arguments together, we could take advantage of the flexibility of Boost.Bind to couple multiple arguments with the function. This is shown below:

void run(int id, char* name)
{
   // Do work ...
}

void someFunc()
{
   // Other code ...

   vpr::Thread* thread;
   thread = new vpr::Thread(boost::bind(run, 50, "My Thread"));
}

There is a limit to the number of parameters that can be passed in this way, so this approach must be used judiciously. We will explain more about function objects and Boost.Bind in the section called “Thread Functors”.

Waiting for a Thread to Complete

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.

Suspending and Resuming a Thread's Execution

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();

If something goes wrong when suspending or resuming, vpr::IllegalArgumentException is thrown. Otherwise, these methods return nothing upon successful completion.

Getting and Setting a Thread's Priority

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);

If something goes wrong when querying or changing the priority of the thread, vpr::IllegalArgumentException is thrown. Otherwise, these methods return nothing upon successful completion.

Sending Signals to a Thread

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 throws vpr::IllegalArgumentException if an error occurs. Otherwise, this method returns nothing upon successful compltion.

Canceling a Thread's Execution

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.

Requesting the Current Thread's Identifier

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 Gory Details

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

  • vpr::ThreadWin32: A wrapper around Win32 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::ThreadWin32 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.

Note

VPR has a wrapper around Netscape Portable Runtime (NSPR) threads. 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.

Thread Functors

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.” In VPR functors are 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.

Important

Users of the VPR 1.0 thread API are encouraged to read this section very carefully. At the end, there is an explanation of how to update VPR 1.0 code to use the new thread functor interface. The flexibility offered by the new approach in VPR 1.1 and beyond should offer programmers many new opportunities for how they handle and utilize threads in their software.

High-Level Description

As mentioned, a functor is used in VPR with vpr::Threads. VPR threads utilize Boost.Function as the functor implementation. A Boost.Function object is a callable object, meaning that it has an overload of operator() that takes zero or more arguments. Earlier in the section called “Creating Threads”, we saw how to use raw function pointers. Boost.Function can wrap four different types of callable types:

  1. Free functions

  2. Static class functions

  3. Non-static member functions

  4. Objects overloading the member function operator()

Programmers already familiar with the use of Boost.Function and Boost.Bind can skip this entire section about thread functors. The remainder of this section is an overview of how to use Boost.Function and Boost.Bind for programmers who are unfamiliar with these tools.

Getting back to functors, 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. What is important to know is that a functor can be thought of as a normal function. When using them, programmers usually just implement a function and then pass the function pointer to the Boost.Function constructor. Boost.Function does the rest.

The functor must behave as a function that returns nothing and takes no parameters. In terms of function pointers, the type must be void(*)(void). A more readable form of this is the Boost.Function type: boost::function<void()>. Yet another form is the portable Boost.Function type used for older compilers: boost::function0<void>. Remember that we are not restricted to using function pointers to create functions. We are describing here callable objects that behave as functions.

If the functor needs to be passed arguments, then Boost.Bind must used to couple parameters with the functor. Those parameters will be passed into the function later when the functor is invoked. Boost.Bind is very powerful, and the full usage of it is beyond the scope of this document. We will demonstrate its use more fully later in this section, but we provide some uses in Example 5.1, “Using Boost.Bind to Create Thread Functors”.

Example 5.1. Using Boost.Bind to Create Thread Functors

struct ThreadArgs
{
   int id;
   std::string name;
};

void run1(const ThreadArgs& args);
void run2(const int id, const std::string& name);

class MyClass
{
public:
   // Bind a free function with an instance of an aggregate type.
   void spawnThread1()
   {
      ThreadArgs args = { 50, "My Thread" };
      mThread = new vpr::Thread(boost::bind(run1, args));
   }

   // Bind a free function with an instanced of an aggregate type that
   // is a data member.
   void spawnThread2()
   {
      // Copies mArgs.
      mThread = new vpr::Thread(boost::bind(run1, mArgs));

      // Passes a reference to mArgs.
      mThread = new vpr::Thread(boost::bind(run1, boost::ref(mArgs)));
   }

   // Bind a free function with multiple parameters.
   void spawnThread2()
   {
      mThread =
         new vpr::Thread(boost::bind(run2, 50,
                                     std::string("My Thread")));
   }

   // Bind a static member function with an argument.
   void spawnThread3()
   {
      mThread = new vpr::Thread(boost::bind(MyClass::staticRun, 10));
   }

   // Bind a non-static member function.
   void spawnThread4()
   {
      mThread = new vpr::Thread(boost::bind(&MyClass::run, this));
   }

   // Bind a non-static member function with multiple parameters.
   void spawnThread5()
   {
      mThread = new vpr::Thread(boost::bind(&MyClass::run, this,
                                            3.14159f, 16));
   }

private:
   static void staticRun(int id);

   void run();

   void otherRun(const float someParam, short otherParam);

   vpr::Thread* mThread;
   ThreadArgs   mArgs;
};

Once a functor object exists, it is passed to the vpr::Thread constructor, and the new thread will execute the functor (which knows what to do with its held callable). The end result is the same as using a normal C/C++ free 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 Boost.Bind with Boost.Function, that is all handled behind the scenes so that passing a non-static member function is straightforward. We have seen how to do this in the methods MyClass::spawnThread4() and MyClass::spawnThread5().

Before getting into specifics, there is a header file that must be included to use Boost.Function VPR thread functors. In this case, the header is boost/function.hpp. If Boost.Bind is needed, then boost/bind.hpp must be included. Finally, if boost::ref() is needed, then boost/ref.hpp must be included.

Functors from (Non-Static) Member Functions

We have already seen examples of how to create a Boost.Function functor for a member function. In this section, we will review in more detail using Boost.Function and Boost.Bind to accomplish this. Just as with free functions, the member functions (heretofore referred to as “methods”) must have the following prototype:

void methodName();

A common example of using vpr::Thread with a member function as the functor callable is shown in Example 5.2, “Member Function for Thread Functor (1)”. The key aspect in this example is the implementation of the method MyObject::start(). The usage of Boost.Bind here is required because the object pointer has to be bound with the member function pointer in order for it to be invoked properly. Fortunately, this use is very simple—much simpler than the use of vpr::ThreadMemberFunctor<T> in VPR 1.0.

Note

This is not the only way of using vpr::Thread, nor is it strictly a recommended way of using it. It is simply an example. One missing aspect is protection of mRunning using a synchronization primitive such as a condition variable.

Example 5.2. Member Function for Thread Functor (1)

class MyObject
{
public:
   MyObject()
      : mThread(NULL)
      , mRunning(false)
   {
   }

   ~MyObject()
   {
      if ( mRunning )
      {
         stop();
      }

      if ( NULL != mThread )
      {
         delete mThread;
         mThread = NULL;
      }
   }

   void start()
   {
      mThread  = new vpr::Thread(boost::bind(&MyObject::run, this));
      mRunning = true;
   }

   void stop()
   {
      mRunning = false;
      mThread->join();
   }

private:
   void run()
   {
      while ( mRunning )
      {
         // Do work ...
      }
   }

   vpr::Thread* mThread;
   bool         mRunning;
};

Now let us say that there is the method MyObject::run() needs to take one or more arguments. Boost.Bind makes this possible, and it is just as easy as what we have seen in the previous example. We simply have to bind the method arguments along with the this pointer, as shown in Example 5.3, “Member Function for Thread Functor (2)”. For the purposes of this example, we have changed MyObject::start() so that it takes an argument that specifies how many iterations the thread loop will perform before exiting.

Example 5.3. Member Function for Thread Functor (2)

void MyObject::start(const int runCount)
{
   mThread = vpr::Thread(boost::bind(&MyObject::run, this, runCount));
}

void MyObject::run(const int runCount)
{
   for ( int i = 0; i < runCount; ++i )
   {
      // Do work ...
   }
}

What if the thread spawning is happening externally to the class that does the work? Once again, Boost.Bind will be necessary, and as we will see, the usage is familiar. Instead of using this, we use the object instance created by the external code. We will change the declaration of MyObject and assume that there is an aggregate type thread_args_t declared somewhere. The code shown in Example 5.4, “Member Function for Thread Functor (3)” demonstrates how we make this happen, though it is a contrived example. Note that the memory allocated for args would have to be deleted at some point after the thread is done using the data. That could be done at the end of MyObject::run() or after the thread is known to have completed its execution.

Example 5.4. Member Function for Thread Functor (3)

struct thread_args_t;

class MyObject
{
public:
   void run(thread_args_t* args)
   {
      // Do work ...
   }
};

void spawnThread()
{
   MyObject* my_obj = new MyObject();
   thread_args_t* args = new thread_args_t();
   // Fill in the arguments to be passed to the thread...

   vpr::Thread* thread = new vpr::Thread(boost::bind(&MyObject::run,
                                                     my_obj, args));
}

Creating so many heap-allocated objects is rather a hassle. Parameters passed to boost::bind() are copied by default. This allows the memory to be coupled with the functor so that it is available when the functor is invoked later. This means that it is safe to use stack-allocated memory when calling boost::bind(). It is not always desirable to have all the data copied, and that is where boost::ref() comes into the picture. In Example 5.5, “Member Function for Thread Functor (4)”, we see an example of using boost::ref() to create a reference to the instance of MyObject while copying in the object of type thread_args_t. Since my_obj is passed by reference, the memory should not actually be allocated on the heap. Rather, it would normally be a data member for the class creating the thread. More generally, my_obj cannot be deleted before the created thread exits. If my_obj is deleted while the thread is running, the application will crash. This is true of all data bound to the function called by the spawned thread.

Example 5.5. Member Function for Thread Functor (4)

MyObject my_obj;
thread_args_t args;

// Fill in the arguments to be passed to the thread...

vpr::Thread* thread = new vpr::Thread(boost::bind(&MyObject::run,
                                                  boost::ref(my_obj),
                                                  args));

Important

Objects bound as parameters to a function call through boost::bind() may be copied many, many times. If this copying of objects will be expensive, strongly consider using boost::ref() to prevent this from happening. Bear in mind that memory may have to be managed differently to ensure that the referenced bound parameter does not get deleted prematurely.

Functors from Static Member Functions

Example 5.6. Static Member Function for Thread Functor (1)

class MyObject
{
public:
   MyObject()
      : mThread(NULL)
   {
   }

   ~MyObject()
   {
      // Need to ensure that the thread is not running.
      if ( NULL != mThread )
      {
         delete mThread;
         mThread = NULL;
      }
   }

   void start()
   {
      mThread  = new vpr::Thread(MyObject::run);
   }

   void stop()
   {
      // Tell the thread to exit ...
      mThread->join();
   }

private:
   static void run()
   {
      // Do work ...
   }

   vpr::Thread* mThread;
};

Example 5.7. Static Member Function for Thread Functor (2)

void MyObject::start(const int runCount)
{
   mThread = vpr::Thread(boost::bind(MyObject::run, runCount));
}

void MyObject::run(const int runCount)
{
   for ( int i = 0; i < runCount; ++i )
   {
      // Do work ...
   }
}

Example 5.8. Static Member Function for Thread Functor (3)

struct thread_args_t;

class MyObject
{
public:
   static void run(thread_args_t* args)
   {
      // Do work ...
   }
};

void spawnThread()
{
   thread_args_t* args = new thread_args_t();
   // Fill in the arguments to be passed to the thread...

   vpr::Thread* thread = new vpr::Thread(boost::bind(MyObject::run,
                                                     args));
}

Functors from Non-Member Functions

Functors for threads can be created for non-member functions. The process is basically the same as for using static member functions. The only real difference is that the class scoping does not need to be used since the non-member functions will not be in a class. A simple example of this is shown in Example 5.9, “Non-Member Function for Thread Functor”. As usual, proper memory management would be needed for the allocated vpr::Thread object. And, of course, parameters to the non-member function can be bound to the function using Boost.Bind.

Example 5.9. Non-Member Function for Thread Functor

void run()
{
   // Do some work ...
}

void startThread()
{
   vpr::Thread* thread = new vpr::Thread(run);
}

Functors from Callable Objects

A new capability not (easily) available with VPR 1.0 is the use of callable objects as functors. This means using an instance of a type that overloads operator() as the callable handled by Boost.Function. One way of doing this is shown in Example 5.10, “Callable Object for Thread Functor (1)”. Note that a copy of c will be made for use when the functor is invoked in the spawned thread.

Example 5.10. Callable Object for Thread Functor (1)

class Callable
{
public:
   void operator()()
   {
      // Do some work ...
   }
};

void startThread()
{
   Callable c;
   vpr::Thread* thread = new vpr::Thread(c);
}

A more interesting use of a callable object would be one that has state. There are two ways of using a stateful callable object. The first is to use data members in the class (or struct) declaration that are then accessed by the overload of operator(). This is a very simple thing to do since having data members in a class is so common.

The other is to use our old friend Boost.Bind, though it is not as simple as in previous cases. Since Boost.Bind is designed for binding parameters to function pointers, we have to use a proper function pointer—or use Boost.Function. In Example 5.11, “Callable Object for Thread Functor (2)”, we see how Boost.Bind is used to bind the parameter value 5 to the invocation of c.operator()(int). This is not nearly as simple as what was shown in Example 5.10, “Callable Object for Thread Functor (1)”, even if data members of Callable had to be initialized.

Example 5.11. Callable Object for Thread Functor (2)

class Callable
{
public:
   void operator()(int arg)
   {
      // Do some work ...
   }
};

void startThread()
{
   Callable c;
   vpr::Thread* thread =
      new vpr::Thread(boost::bind(&Callable::operator(), c, 5));
}

Since we are working with a callable object, it stands to reason that we should be able to leverage Boost.Function somehow. In Example 5.12, “Callable Object for Thread Functor (3)”, we see how boost::function<T> can be used to reduce the apparent complexity of binding the parameter value 5 to the invocation of c.operator()(int). To some, this may look a little more friendly since there is no member function pointer required. Ultimately, it requires slightly more typing than the previous example. Either way, neither of these approaches are very pleasing to the eye, but they do work. One of these approaches may be necessary to use a legacy callable type with vpr::Thread.

Example 5.12. Callable Object for Thread Functor (3)

class Callable
{
public:
   void operator()(int arg)
   {
      // Do some work ...
   }
};

void startThread()
{
   Callable c;
   vpr::Thread* thread =
      new vpr::Thread(boost::bind(boost::function<void(int)>(c), 5));
}

Updating from VPR 1.0

In VPR 1.0, thread functors were handled by subclasses of vpr::BaseThreadFunctor. While this approach worked, it was not anywhere near as flexible as what is possible with Boost.Function and Boost.Bind. What has been presented here is for VPR 1.1 and beyond. For those users updating from VPR 1.0, we now present the simple process of changing code using the VPR 1.0 thread API to the VPR 1.1 API.

First, the signature for the function called by the spawned thread in VPR 1.0 took a single void* parameter. In VPR 1.1, the function takes no argument. This change was made for two reasons. First, most functions ignored this parameter, so it was wasted memory on the stack. Second, void* is a notoriously poor choice for a type since it can point to anything. C++ is a strongly typed language, and we should be taking advantage of that feature. However, since the presence of the parameter acknowledged that it was often necessary to pass data into the thread function, it is still possible to do this using Boost.Bind. What this means is that the callable invoked by the functor can have any signature, but the default behavior is for it to return nothing and take no parameters.

Next, the types vpr::ThreadNonMemberFunctor, vpr::ThreadMemberFunctor<T>, and vpr::ThreadRunFunctor<T> have been removed, as has the header file vpr/Thread/ThreadFunctor.h. The functionality provided by those types has been offloaded to Boost.Function and Boost.Bind. In so doing, the flexibility of how threads are spawned has been increased immensely while actually making it simpler to spawn them.

For uses of vpr::ThreadNonMemberFunctor, quite a bit less code has to be written to spawn a thread. We see in Example 5.13, “VPR 1.0 Use of Thread Non-Member Functor” how this type was used with VPR 1.0 to spawn a thread that executed a static member function. Then, in Example 5.14, “Updated Use of Static Member Function for Thread Functor”, we see the equivalent code using the VPR 1.1 (and newer) interface. No longer must a functor object to instantiated on the heap and stored for later deletion[4]. Moreover, there is no need to pass NULL as the parameter value, though if a parameter value is needed, then Boost.Bind must be used. Refer to the section called “Functors from Static Member Functions” for more details on this topic.

Example 5.13. VPR 1.0 Use of Thread Non-Member Functor

#include <vpr/Thread/ThreadFunctor.h>
#include <vpr/Thread/Thread.h>

class MyClass_VPR10
{
public:
   void start()
   {
      mFunctor =
         new vpr::ThreadNonMemberFunctor(MyClass_VPR10::run, NULL);
      mThread = new vpr::Thread(mFunctor);
   }

private:
   static void run(void* args)
   {
      // Do some work ...
   }

   vpr::ThreadNonMemberFunctor* mFunctor;
   vpr::Thread* mThread;
};

Example 5.14. Updated Use of Static Member Function for Thread Functor

#include <vpr/Thread.h>

class MyClass
{
public:
   void start()
   {
      mThread = new vpr::Thread(MyClass::run);
   }

private:
   static void run()
   {
      // Do some work ...
   }

   vpr::Thread* mThread;
};

To use a non-static member function, either vpr::ThreadMemberFunctor<T> or vpr::ThreadRunFunctor<T> was used with VPR 1.0, as shown in Example 5.15, “VPR 1.0 Use of Thread Member Functor”. Now, Boost.Bind comes to our rescue and vastly simplifies the code needed to accomplish the very same thing. In Example 5.16, “Updated Use of Member Function for Thread Functor”, we see just how much less code is required. The nice thing is that the parameters that were passed to the vpr::ThreadMemberFunctor<T> constructor are nearly the same as what must be passed to boost::bind(). The order for the this pointer and the member function pointer are simply reversed—and the NULL value for the function argument is removed. If arguments need to be passed to the function, pass them in as arguments to boost::bind() after the this pointer. Refer to the section called “Functors from (Non-Static) Member Functions” for more details on this topic.

Example 5.15. VPR 1.0 Use of Thread Member Functor

#include <vpr/Thread/ThreadFunctor.h>
#include <vpr/Thread/Thread.h>

class MyClass_VPR10
{
public:
   void start()
   {
      mFunctor =
         new vpr::ThreadMemberFunctor<MyClass_VPR10>(
            this, &MyClass_VPR10::run, NUL
         );
      mThread = new vpr::Thread(mFunctor);
   }

private:
   void run(void* args)
   {
      // Do some work ...
   }

   vpr::ThreadMemberFunctor<MyClass_VPR10>* mFunctor;
   vpr::Thread* mThread;
};

Example 5.16. Updated Use of Member Function for Thread Functor

#include <boost/bind.hpp>
#include <vpr/Thread/Thread.h>

class MyClass
{
public:
   void start()
   {
      mThread = new vpr::Thread(boost::bind(&MyClass::run, this));
   }

private:
   void run()
   {
      // Do some work ...
   }

   vpr::Thread* mThread;
};



[4] Leaking the instantiated functor was an all-too-common error with VPR 1.0.

Chapter 6. Synchronization

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.

Semaphores: Using vpr::Semaphore

The most important part of multi-threaded programming is proper thread synchronization so that access to shared data is controlled. Doing so results in c