[cpp-threads] Re: Exception propagation across threads
Peter A. Buhr
pabuhr at plg.uwaterloo.ca
Thu Mar 8 22:22:26 GMT 2007
Sorry for the delay in getting this out, but I've been busy.
Since a general question about exceptions with threads has been asked, I
thought I'd mention what exists in uC++, which is a dialect of C++ supporting
concurrency (and other advanced control flow mechanisms) at the object
level. Most of this discussion is implemented, available, and documented in:
http://plg.uwaterloo.ca/~usystem/pub/uSystem/uC++.pdf
for version uC++ 5.4.1, but some of the material I'll mention is in uC++ 5.5.0,
which will be released in a few months. I appreciate that much of what I say
is too radical for the standards committee; nevertheless, it might give some
food for thought to others.
uC++ supports advanced exception handling over the standard exception handling
provided in C++. Many of these changes are well-known concepts advocated by
exception researchers and existing in other languages. These changes are:
1. All uC++ exception types publicly inherit from a common base-type, grouping
them into a single hierarchy. uC++ extends the small C++ set of predefined
exception-types with the uC++ exceptional runtime and I/O events. (like Java
exceptions)
2. uC++ restricts raising of exceptions to the specific exception-types; C++
allows any instantiable type to be raised (e.g., "throw 3;").
3. uC++ supports two forms of raising: throwing and resuming; C++ only supports
throwing. All uC++ exception-types can be either thrown or resumed. uC++
adopts a propagation mechanism eliminating recursive resuming, even for
concurrent exceptions.
4. uC++ supports two kinds of handlers, termination and resumption, which match
with the kind of raise; C++ only supports termination handlers.
Unfortunately, resumption handlers must be simulated using routines/functors
because uC++ is implemented using a simple translator/library approach.
5. uC++ supports raising of nonlocal exceptions so that exceptions can be used
to affect control flow among coroutines and tasks. The uC++ kernel
implicitly polls for nonlocal exceptions at specific checkpoints (as for
cancellation). It is also possible to (hierarchically) block these kinds of
exceptions when delivery would be inappropriate or erroneous.
6. uC++ supports cancellation among coroutines/tasks to safely terminate their
execution. Cancelling a coroutine/task does not result in immediate
cancellation of the object; cancellation only begins when the coroutine/task
encounters a cancellation checkpoint. While there is no provision to
``uncancel'' a coroutine/task once it is cancelled, it is possible for the
cancelled coroutine/task to control if and where cancellation starts. Once
cancellation starts, the stack of the coroutine/task is unwound, executing
object destructors as well as catch-any exception handlers to allow safe
cleanup. Unlike a nonlocal exception, cancellation cannot be caught or
stopped unless the cleanup code aborts the program. uC++ cancellation is
also integrated with Pthreads cancellation. (There is a fairly complete
Pthreads simulation available in uC++, through which some OpenMP systems
also work).
Within each of these points there are many interesting details and
capabilities, but I won't go into these issues here.
I will at least address the specific question of transferring exceptions across
a "join". uC++ adopts the suggested semantics of terminating the application if
a task does not handle a thrown exception. However, the reason for this is that
uC++ does not have an explicit "join". Instead, uC++ uses allocation and
deallocation to start a thread and join with it, e.g.:
_Task T { // all the properties of a class, plus separate stack and thread
void main() {...} // thread starts here
};
{
T t; // task object created on stack and thread starts in main
T at[10]; // array of task objects with 10 threads
T *pt = T; // task object created in heap
} // implicit join with "t" and "at" task objects as part of deallocation
...
delete pt; // explicit join with "pt" task object as part of deallocation
The justification for this behaviour is that most threads are started
immediately after allocation and joined just prior to deallocation (in non-GC
systems). For a number of reasons, transferring an exception from the joining
thread out of "delete" is not a good approach. However, explicit start and join
can be easily implemented in uC++ (only join is presented with respect to this
discussion) by using a mutex routine (similar to a "synchronized" member in
Java), e.g.:
#include <uC++.h>
#include <iostream>
using namespace std;
_Event E {}; // create an exception type
_Task T { // create a task type
uBaseEvent *copy; // use base exception type
int result;
public:
int join() { // manual create a join routine (name is not special)
if ( copy == NULL ) return result; // no exception raised ?
else _Throw *copy; // throw raised exception at joiner
}
private:
void main() { // thread starts here
copy = NULL;
try {
... _Throw E(); ... // compute a result, possibly raising an exception
} catch ( uBaseEvent &ex ) { // catch any uC++ exception (like "catch(...)")
copy = ex.duplicate(); // copy exception for joiner without slicing
}
_Accept( join ); // wait for joiner to call
}
};
void uMain::main() {
T t; // create and start task
try {
int result = t.join(); // return type-safe result
osacquire( cout ) << "result:" << result << endl;
} catch( E ) {
osacquire( cout ) << "exception" << endl;
}
}
Using the exception mechanism in uC++, it is possible to implement any kind of
exception interaction for any mutex call, including one that simulates a
traditional "join". It is also possible to wrap the actual raised exception in
a general exception "JoinFailure", and then reraise the actual exception in the
catch for JoinFailure.
Finally, the new version of uC++ has full "future" capability for asynchronous
communication, which supports exception transfer from server to client and
client cancellation of futures.
More information about the cpp-threads
mailing list