[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