document number: n2107
Jens Maurer <Jens.Maurer@gmx.net>
Alisdair Meredith <alisdair.meredith@uk.renaultf1.com>
2006-10-13

Exception Propagation across Threads

Problem Description

During the Redmond Threads Meeting, the participants tentatively agreed upon having exceptions be rethrown when joining a thread that terminated with an uncaught exception. In N2061 "Library Exception Propagation Support", Beman Dawes presents a library-only solution involving a mixin-class. This solution is feasible, but has these disadvantages: This paper outlines core language support for solving the problem. Further revision is needed if support for move constructors is desired.

The following description assumes that a non-detached (sub-)thread T1 throws an exception E that is not caught. Another thread T2, possibly the main thread, executes a join for T1. Subsequently, T1 is terminated and E is thrown from inside the join function in T2.

Option 1: minimum "magic" cross-thread rethrow

Outline

In the library support code for thread T1, catch all exceptions, synchronize with another thread T2 that wants to join, give T2 the opportunity to "steal" the currently handled exception E (15.3p8), synchronize again to ensure the theft is complete, and then terminate T1.

T2 "steals" the exception from T1 by means of a special language-support library function (name subject to change)

  std::rethrow_from_foreign_thread(thread_id_type id)
That function copies the currently handled exception E from T1 to T2 and reactivates it (15.1p7).

Thus, the library support code for T1 might look like this:

  try {
    // call user-supplied function that is executed as T1
  } catch(...) {
    // wait for condition "about to join me"
    // acknowledge join, write "exception pending"
    // exception is "stolen" by T2
    // wait for condition "theft complete"
    // do not use "throw;"
    return;       // T1 terminates
  }
  // wait for condition "about to join"
  // acknowledge join, write "no exception"
  return;    // T1 terminates
The library support code for join (in T2) might look like this:
  void join(thread_id_type id)
  {
    // lock inter-thread synchronization mutex
    // write "id" to global synchronization area
    // signal condition variable "about to join"
    // wait for join acknowledge
    if (exception_pending(id)) {
      try {
        std::rethrow_from_foreign_thread(id);
      } catch (...) {
        // acknowledge "theft complete"
        throw;
      }
    }
    // no exception pending, done
  }
This model yields global serialization for join operations, other solutions that use well-defined per-thread memory for synchronization are also feasible.

Details

A more formal specification of the "magic" function would look like this:
  std::rethrow_from_foreign_thread(thread_id_type id);
Precondition: "id" refers to an existing, not-yet-joined thread. That thread has a currently handled exception (15.3p8) and is waiting on a condition variable.

Effect: Reactivates the currently handled exception from that thread in the context of the caller (see 15.1p7).

Note: It is unspecified whether this function invokes the copy constructor of the exception's class (15.1p5).

Implementation

For exposition purposes, the following presentation assumes that std::type_info objects are used to represent object types thrown as exceptions. In general, each catch handler would bear a reference to a std::type_info. When a handler is matched against an active exception, that exception carries a reference to its type's std::type_info that is matched against the handler's std::type_info. Matching base vs. derived classes requires additional checks that are passed over for this presentation. std::type_info objects are laid out for all types in the program, because C++ imposes few restrictions on which types can be thrown as exceptions.

The std::type_info information could be extended to contain a pointer to the copy constructor. (That pointer would not be exposed outside of the compiler's runtime support, keeping in mind that std::type_info is used in this presentation only for exposition purposes.) The copy constructor always exists and is accessible for types thrown as exceptions (15.1p5).

std::rethrow_from_foreign_thread then copies the exception from the (possibly thread-specific) memory location for "currently handled exceptions" to the local thread's space for the same and performs as if "throw;" would have been written.

If the overhead of extending std::type_info (or the compiler's equivalent) with a pointer to the copy constructor, if any, is deemed excessive, the compiler could hook on all "throw" expressions and emit copy constructor pointers for types that are actually thrown as exceptions. However, this would require additonal machinery to look up said pointer from std::rethrow_from_foreign_thread.

As a third option, the compiler could augment its exception infrastructure to not only transmit the exception object and its corresponding std::type_info, but a copy constructor pointer as well when an exception is actually thrown.

Option 2: Type-Agnostic Cloning and Throwing

Outline

A general feature that allows cloning of the currently handled exception (using heap-allocated memory) and rethrowing it later is introduced, for example by introducing a special language-support library function std::currently_handled_exception and extending std::type_info with additional member functions
  void * currently_handled_exception();
  class type_info {
    // ...
    void* clone_exception(const void *);
    void destroy(void *);
    void rethrow(const void *);
  };
and introducing typeid(...) that would return a reference to the std::type_info of the currently handled exception inside a catch(...) handler.

If the type identified by a given std::type_info instance does not have a publicly accessible copy constructor, clone_exception returns a null pointer.

Thus, the library support code for T1 might look like this:

  try {
    // call user-supplied function that is executed as T1
  } catch(...) {
    const std::type_info& ti = typeid(...);
    void * exc = ti.clone_exception(std::currently_handled_exception());
    // move "&ti" to joining thread T2
    // move "exc" pointer to joining thread T2
    return;       // T1 terminates
  }
  // indicate "no exception" to joining thread
  return;    // T1 terminates
The library support code for join (in T2) might look like this:
  void join(thread_id_type id)
  {
    // get the "exc" and "type_info" pointers
    const void * exc = ...; // get from T1
    if (exc) {
      const std::type_info * ti = ...; // get from T1
      try {
        ti->rethrow(exc);
      } catch(...) {
        ti->destroy(exc);
        throw;
      }
    }
    // no exception pending, done
  }
(Similar inter-thread co-ordination as outlined for option 1 may be necessary.)

Details

In section 5.2.8, add after paragraph 4
typeid(...) shall appear lexically inside an exception handler declared with catch(...) only. In such use, the result refers to a std::type_info object representing the type of the currently handled exception.
Language support library (clause 18):
  void * currently_handled_exception();
Effect: Returns a pointer to the currently handled exception (15.3p8), or a null pointer if there is none.
  class type_info {
    // ...
    void* clone_exception(const void *);
    void destroy(void *);
    void rethrow(const void *);
  };
  void* clone_exception(const void * p);
Precondition: The parameter p has a value that was previously returned by the function currently_handled_exception.

Effect: Allocates memory and invokes the copy constructor on the object that p points to. It is unspecified whether the memory is on the heap or some other memory accessible to all threads.

Returns: Returns a pointer to a copy of the object that p points to, or a null pointer if p is a null pointer.

Throws: bad_alloc if memory could not be allocated, or bad_clone if the class of the object that p points to has no publicly accessible copy constructor.

  void destroy(void * p);
Precondition: The parameter p has a value that was previously returned by the function clone_exception.

Effect: Invokes the destructor of the object that p points to and frees the associated memory. Does nothing if p is a null pointer.

  void rethrow(const void *);
Precondition: The parameter p has a value that was previously returned by the function clone_exception.

Throws: The object that p points to, or nothing if p is a null pointer.

Implementation

Similar to option 1, the implementation has to keep track of publicly accessible copy constructors in std::type_info for use by clone_exception. Type-agnostic destructor calls are already required by the current language rules.