Safe, Easy Resource Protection using POSIX Threads in C++
by
Nathan J. Oostendorp
Robert I. Murillo
Abstract
The design and implementation
of a correct threaded system is a difficult task. Novice parallel
programmers
often find it difficult to implement correct systems in serial
languages because of the awkward syntax and the lack of debugging
tools. This paper discusses an attempt to help protect beginning parallel
programmers against common concurrent system errors. It describes
Dynac++, a set of C++ objects that implements protections against
two common concurrency anomalies: data corruption and deadlock.
Introduction
Threads are becoming an increasingly important part of
software
development. Programs that take advantage of threaded operating systems are
increasingly important for optimizing use of system resources.
POSIX threads implement
some very low-level resource protection, but it is up to the programmer
to know when to use these methods and how to properly employ them. The
process of
figuring out how to use this low-level protection can be a complex and
tedious task. Figuring out when and how to protect
data is difficult because it is necessary to think about programming in
an entirely different paradigm. Programmers trained on serial systems can
have difficulty visualizing and understanding parallel concepts and must
learn how to think of a program
in terms of several procedures running at the same time. The premise of
the Dynac [1] project is
that if a generalized, reliable set of methods were available to insure
data correctness, developing threaded applications would be faster and
easier, and less intimidating for the beginning threads programmer.
There are several aspects of POSIX threads that make
their
use difficult. One such aspect is the thread creation and manipulation
syntax. Simply creating a simple threaded system with simple locks
requires manual initialization and destruction for all components.
While this gives the programmer a high degree of control, it can
be confusing and tedious for beginners.
Another area where POSIX threads is lacking is
built-in resource protection. There is no standard method to prevent
data corruption and deadlock, two major problems that are encountered in
the implementation of parallel systems. When several different threads are
dealing with the same set of data, there is potential for two threads to
attempt to access the same piece of data at the same time in ways
that will corrupt the data. A classic example is the ATM problem.
Consider an example: Fred Madison and his wife Renee
simultaneously decide that they need some cash. They
both go to nearby ATMs. Fred places his card in the machine, and Renee
does likewise. They both request a withdrawal, and their respective
machines request an account balance from the bank's main computers. Both
ATMs are informed that the Madisons have $700 in their checking account.
Fred tells his ATM to give him $30, which the teller machine does and
writes to the bank
that the Madisons now have $670 in their account. Renee simultaneously
withdraws $40, and her ATM tells the bank that they have $660 in their
account. The net withdraw on their account is $70, but only $40 has
been accounted for, due to data corruption.
If we abstract
Fred and Renee to threads and the bank's computer to a shared set of
data, the need for resource protection becomes clear.
Data corruption can potentially go undetected until well after it has
occurred. Deadlock, however, tends to become evident immediately.
Deadlock occurs when two threads are both waiting for each other to be
done with different pieces of data while holding locks on the data that
the other thread is waiting for. For example, thread A begins
with a lock on object 1. Thread B begins with a lock on object 2.
Thread A requests a lock on object 2, and blocks since object 2 is already
locked by Thread B. Now if Thread B, while still holding object 2
requested a lock on object 1, deadlock would occur. Neither thread
would continue operation, since they would both be waiting for the
other to release the lock. This cyclic dependency between threads
halts operation and is known as deadlock.
In our paper we wish to outline the need for a system
that
would allow users to easily write threaded programs in C++, the current
options available, and our implementation of the Dynac++ system. We
will also discuss our performance testing and what could be done
to improve our system.
Current State of the Art
POSIX threads are currently supported on most major operating systems.
They contain a set of standardized functions which allows the user to
create and destroy threads and locking mechanisms. However, these
are not object oriented, and so, in order to stay cleanly within an
OOP system, a C++ programmer would write their own class wrappers
around the threads.
Many threaded applications are developed in C and C++. However, the
languages were designed assuming a
serial programming model. Parallel programming requires different
methods of program design and implementation. These languages
were not developed specifically to be used for parallel programming, so
it is often awkward to apply parallel techniques. One solution to this
problem has been under development at MIT. The Cilk project
[2] focuses on
developing a language based on C that is specifically designed for
multithreaded parallel programming.
In 1997, a research group at Hope College took a different approach.
Based on some algorithms designed by Jipping, this group
coded Dynamic Anomaly Correction in Java [3]. This system is set up to make
threaded programming less intimidating for beginners by taking care of
the resource protection issues that are probably the most difficult
aspect of threaded programming to become accustomed to.
This system is
implemented in Java, which has threads natively implemented in the
language. Java has procedure-level protection using the
"synchronized" keyword. The Dynac classes allowed for data-level
protection, with each "protected object" having it's own
internal locking mechanism. Using this package,
Java programmers could design their programs to safely use threads with
minimal effort.
A Model for Resource Protection
In his paper "Developing a Formal Model for Concurrent System Design",
Jipping describes a General Process Model (GPM)
system where a central
concurrency control defines the behaviour of parallel processes.
This control can be designed very abstractly, so that many different systems
can use it, or very specifically as to optimize performance. Using this
system, a programmer could quickly prototype a parallel system using an abstract
concurrency control. Then, if necessary, the programmer could create or alter the
concurrency control to work more optimally for his specific system. This type
of abstraction can be especially beneficial for beginning parallel programmers,
who might be willing to sacrifice a certain degree of control, in order to not
have to deal with parallel problems such as deadlock and data corruption.
During the summer of 1997 a research team worked with Dr. Jipping to
develop the Dynac System. As with Jipping's original
system, Dynac had an abstract concurrency control class that could be used with
a variety of concurrency controls. However, the Dynac system was
developed with the intention of making Java threads programming
easier and safer, rather than being a full parallel design system, as
Jipping's GPM was designed to be.
Dynac++
The new Dynac++ system is implemented in C++. It consists of many of the
same classes as
the old Dynac system, with a few alterations for the transition from Java to C++, as well
as some corrections on the DeadLockDetection module. Since C++ does not
contain a native
threads class, a SafeThread class was created to encapsulate it. A protected
object (PObject) class was also designed for use with SafeThread.
POSIX
Threads were
used in the Dynac++ implementation to ensure cross-platform compatibility.
The PObject class is the central class of the Dynac++ system. It contains methods
for data/thread interaction, as well as an internal version table to ensure
data correctness as threads request access. A user would descend their data object
class from PObject, and then use the appropriate methods to communicate thread actions
upon it. When the user initializes the PObject, they are required to register the
PObject with a concurrency control. The PObject will keep a pointer to this control
so that it can communicate with it when requests are made, and it also registers
itself with the control to receive a unique ID, for tracking that object's
dependencies.
The methods implemented in the PObject are read() for a reader request, write() for a writer request,
and done() to release any outstanding requests. When these methods are called, the PObject
will return 1 if access has been granted or a 0 if access has been denied due to
denial of the request from the concurrency control. In addition to the
concurrency control communication, the PObject keeps an internal table of versions
for each thread. Any time a write() request is granted, a version counter is
incremented, meaning that the PObject's internal data could potentially have
been changed. A thread that was granted a read lock on a PObject with version 4, but
then asked for a write lock when the version was 6 would receive a corrupted data
error, since it would be trying to overwrite a new version of the PObject.
The SafeThread class is the class with which the user/programmer has the
most direct interaction, as is seen in figure 1. SafeThread encapsulates a standard POSIX p_thread, providing
methods for the object for creation and joining, as well as methods for interaction
with Protected
Objects. It is modeled after the Java Thread class, and uses a similar style where the
user descends their class from the thread class for operation.
When the user initializes a SafeThread, he or she can register it with a
concurrency control that they select. The concurrency control will assign
a unique ID to that thread which the ConcControl and PObject classes will use
to keep track of thread information. The SafeThread also contains the read(),
write(), and done() methods, which are the user's primary means of
communicating with the PObject. These methods take a PObject as their single
parameter, and communicate to that PObject the Thread's unique ID. The PObject,
in turn, then communicates this information as well as its own unique ID to
the
concurrency control, which uses a set of specific algorithms to decide if and when
the thread should be granted access to the Object.

Figure 1: Relationships between Dynac++ Objects
The ConcControl class is an abstract class, from which a variety of concurrency
controls are descended. The ConcControl class contains the read(), write(), and
done() methods as well, but these methods contain specific information
for the Thread and PObject involved. The three most important specific instances
of the ConcControl class are Dynamic, Conservative, and
Single-Writer-Multiple-Reader. Other instances of ConcControl could be created
by a programmer to provide fine-grain control of a specific system.
The Dynamic class is descended from ConcControl and implements the simplest form of
control: no control. Being purely dynamic, the Dynamic class grants all requests
that are made upon it. The logic behind this is that the user is using the PObject's
internal algorithm to detect data corruption to ensure data, rather than locking
threads to try and protect the data. In this system, the user would be more
interested in correcting data when an error occurs than preventing corruption.
The potential benefit of using a purely dynamic control is that no locking occurs,
which means that performance will be uninhibited by blocking threads.
On the opposite end of the spectrum is the Conservative class. Conservative is a
strict conservative implementation of ConcControl where only one thread is allowed
to have a read or write lock on a PObject at one time. This is the "safest" of
concurrency controls, since each thread has solitary access. However,
Conservative is also the most detrimental to performance since even threads that
only require reading locks, which could be granted simultaneously, are given
exclusive access to the PObject.
Between these two controls is the Single-Writer-Multiple-Reader class. This
class allows multiple reader locks to be granted simultaneously, while writer locks
must be made exclusive to any other lock. Reader locks are granted if there are
no outstanding writer locks or requests, and writer locks are granted if there are
no outstanding reader locks, and only one writer lock may be granted at a time. When
a writer request is made, all reader requests are blocked, and then the writer lock
is granted when there are no outstanding reader locks left. This compromise between
dynamic and conservative concurrency controls allows a degree of freedom to enhance
performance, without leaving the object unprotected.
Both Conservative and SWMR, which have forms of conservative locking
mechanisms,
use the DeadlockDetect class. The DeadlockDetect class is called by these
concurrency controls whenever a strict conservative lock is granted. The
module keeps a chart of Threads and their Object dependencies, to make sure that
the requested lock would not cause a deadlock situation. If a deadlock situation
would occur, the deadlock module returns an error and the request is not granted.
The idea of deadlock detection is to find cyclic dependencies between threads.
The DeadlockDetect module accomplishes this through keeping two tables: a thread
request
table, and an object lock table. Whenever a request for a conservative lock on an
Object is made,
the object lock table is checked as to whether any threads already have locks
on that object. If the object is unlocked, the thread's ID is inserted into
the object lock table, and access is granted.
If the object is locked, the thread request table is accessed to
see if the thread holding the lock has any outstanding requests.
If it does, the object lock table
is accessed again for this requested object. This continues until DeadlockDetect
traces the dependency to a thread with no outstanding requests, or the object
lock table points to the thread itself. In the former case, the threads request is
inserted in the thread request table. In the latter, a deadlock situation has
occurred and the DeadlockDetect module sends an error back to the concurrency
control.
A classic example of a potential deadlock situation is the Dining Philosophers
problem. In this problem we have five philosophers sitting around a table, with
five forks, one between each philosopher. In order to dig in to the heaping piles
of spaghetti on their plates, each philosopher must have two forks. The problem
is to let all the philosophers eat (sharing the fork resources), and think. An
incorrect solution to this problem would be to have each philosopher grab their
right fork, then have each philosopher grab their left fork. If each philosopher
were to grab their right fork at the same time, there would be no forks on the table,
so no one could eat. This is a cyclic dependency, and a deadlock situation. For an example of a correct
solution to the dining philosophers problem see Tannenbaum [4].
However, with the introduction of the DeadlockDetect class, that incorrect answer
becomes feasible. Consider this algorithm for the Philosophers:
pick up my right fork
pick up my left fork
if left fork returned deadlock, drop forks and go to beginning
eat
drop forks
think
go to beginning
Since the philosophers' threads can detect deadlock, the programmer can
take advantage
of it and wait to prevent deadlock until it is about to occur.
Performance Data
Performance improvements have been made from the Dynac++ system over the Dynac
system. Many inefficiencies in the code were revised with the translation in
to C++ especially in the DeadlockDetect module. Whereas Dynac took a
noticeable performance hit with deadlock detection engaged, for Dynac++
the difference in run-time testing was negligible.
There is still a significant decrease in performance when using the Dynac
system as opposed to a specifically designed algorithm as is shown in
figure 2. For this test we implemented Tannenbaum's solution to the dining
philosopher's problem using POSIX threads and locks, and we also implemented it
using Tannenbaum's solution, but relying on the Dynac system for conservative
locking. These real time tests were made on a Sparc Ultra 1 running Solaris
2.6.

Figure 2: Real Time Comparison of Dining Philosopher Test
(10000 eat/think cycles per Philosopher)
The performance detriment was anticipated, since every resource access (6 per
cycle) must call several functions to chart versions, anticipate deadlock, etc.
in the Dynac model, whereas in Tannenbaum's model a simple lock/unlock call is
made. However, this is considered an acceptable loss since beginning
parallel programmers are typically not interested in
writing performance-intensive threaded
applications, as much as easily learning the concepts of threaded programming.
Conclusions
In our paper we have discussed some concerns and difficulties novice
parallel programmers have had using POSIX threads in C++. We addressed
how the Dynac++ system deals with these issues, the implementation
of Dynac++, as well as the tradeoffs for using an abstracted system.
In the future, Dynac++ could be developed into a more fully-featured
set of C++ classes to aid in parallel programming. More specific
concurrency controls could be added, as well as mechanisms for
developing more complex systems within the Dynac framework.
Bibliography
- VanEngen, Anita J. Bradshaw, Michael K. Oostendorp, Nathan. "Extending
Java to Support Shared Resource Protection and Deadlock Detection in
Threads Programming"
- Leiserson, Charles E. The Cilk Project. http://theory.lcs.mit.edu/~cilk/
- Jipping, Michael J. "Developing a Formal Model for Concurrent System Design"
- Tannenbaum, A.S., Operating Systems: Design and Implementation , Prentice-Hall, 1987.
Acknowledgements
We would like to tip our collective hat to the Hughes and NSF REU program, who
provided funding for this research. We would also like to thank Hope College
and their Computer Science department staff, whose facilities hosted us for our
10 week stint. Finally, we wish to thank our project mentor Prof. Mike Jipping,
whose guidance was invaluable, and whose patience was incalculable.
Biography
Nathan J. Oostendorp is a senior at Hope College and is originally from Zeeland, MI.
He is an English/Computer Science major and keeps busy with everything.slashdot.org,
his pet project. Nate enjoys the Perl programming language, coding projects with
his roommates, nerf skirmishes, and postmodernism.
Robert I. Murillo is a junior at the University of Michigan. He is from
Holland, MI, the home of Hope College, which he has also attended. Bob
is a computer science major, and devotes altogether too much time to school.
For More Information
See http://www.cs.hope.edu/dynac++
|