Exception handling in TBB 2.2 - Getting ready for C++0x

I've never finished the series of the blogs devoted to exception handling and cancellation I started last year, and now seems to be a convenient moment to do this at last. Why now? Because just released TBB 2.2 (feel free to download its open source version from the official TBB site) utilizes the new C++ exception propagation functionality that is part of the upcoming C++0x standard, and thus makes exception handling in TBB based applications even more straightforward. Of course the new mechanism will work only when it is supported by the compiler, otherwise the original TBB semantics is used.

But let's start from the basics that are common to both exception propagation mechanisms. As it was mentioned in the first blog in the cancellation series the primary purpose of the exception handling support in TBB is to make the following construct working:

try {
parallel_algorithm (/* … */);
} catch ( /* SomeException or Ellipsis */ ) {
// log error or take recovery actions and restart the algorithm
} /* … possibly more catch blocks */

The challenge of supporting this simple and natural model is caused by the fact that any parallel algorithms spawns multiple tasks that are executed in different threads, while the try block guards only the current thread.

In TBB this problem was solved by incorporating an exception handler into the dispatch loop of the task scheduler. This handler intercepts any exception that escapes TBB task's execute() method. If it is the first exception in the current task's group, the whole group is canceled, and the information about the exception is stored in the group context. Subsequent exceptions in the same group that may be thrown in other threads are ignored. After all the remaining tasks belonging to the cancelled group finish, the algorithm that created this task group completes and rethrows the exception.

Such process of passing exceptions between threads (or between different parts of code in one thread) is called exception propagation. And this is where TBB 2.2 introduces new functionality. But since the first compiler to support the new model will be C++ of VS2010, you'll have to stick with the original mechanism from TBB 2.1 for some time yet. So let's consider it first.

Since the internal exception handler in the task scheduler needs to catch all exceptions, it separately catches TBB specific exceptions (more below), exceptions derived from std::exception, and at last all the remaining ones using the ellipsis catch block. With today's C++ compilers this means that in general case TBB cannot create a copy of the original exception, which is necessary to rethrow it later. It only can throw an exception of its own to indicate that some other exception was intercepted earlier and caused the algorithm's cancellation. This changeling is called tbb::captured_exception and is derived from the base class tbb::tbb_exception, that in its turn is derived from std::exception.

When the original exception is derived from std::exception, TBB is still able to preserve some useful information about it. It remembers the string returned by std::exception::what() method, and the RTTI name of the original exception. This information can be retrieved by means of the tbb_exception::what() and tbb_exception::name() methods correspondingly.

TBB also provides template class tbb::movable_exception<UserData> that can be thrown by a task in order to deliver arbitrary attached data into the exception handler. The only requirement to the template parameter is to be copy-constructible because exception propagation involves copying (potentially multiple). You attach data via the constructor, and access them by means of the movable_exception::data() method.

An important property of tbb::captured_exception and tbb::movable_exception<T> is that once they are thrown, they are propagated unchanged. This is possible because both of them are derived from tbb::tbb_exception that is recognized by the TBB scheduler and supports propagation by exposing tbb_exception::move() and tbb_exception::destroy() methods. You can use these methods in rare cases when you need to pass TBB exception objects between threads manually. Note that calling move() method on an object rips its contents away by moving it into the dynamically allocated clone that is returned as its result. Don't forget to call destroy() on any exception object allocated by means of move().

Summarizing what was said above, the code fragment outlined below could be used either in more specific form:

try {
parallel_algorithm (/* … */);
} catch ( tbb::captured_exception& ce ) {
std::cerr << "Intercepted exception " << ce.name();
std::cerr << "Reason is " << ce.what();
catch ( tbb::movable_exception& me ) {
HandleExceptionData( me.data() );

or in a more general way:

try {
parallel_algorithm (/* … */);
} catch ( tbb::tbb_exception& e ) {
std::cerr << "Intercepted exception " << e.name();
std::cerr << "Reason is " << e.what();

Now it's high time to see how TBB 2.2 changes the exception propagation. In fact it can be described in five words - it propagates any exception unchanged.

Thus, if your parallel code can throw, say, std::out_of_range and CMyDataCorruption, then you can write

try {
parallel_algorithm (/* … */);
} catch ( std::out_of_range& e ) {
// handler code
} catch ( CMyDataCorruption& e ) {
// handler code

However, as I already noted above, this new functionality will work only when TBB library is built with a compiler that supports std::exception_ptr extension coming as part of C++0x. And the commercial version of TBB 2.2 is not. Nevertheless, if you use open source TBB, then you can build it with VS2010 Beta or with gcc 4.4 to enable "exact" exception propagation. If you use gcc 4.4, you'd need to specify "-std=c++0x" option during compilation, as it implements new exception propagation semantics only as an experimental extension so far.

To this moment acute readers must have already noticed that though delivering original exceptions unchanged may be a cool feature, it entails a potential backward compatibility problem. Indeed, if an existing application expecting only tbb::captured_exception would get a bunch of other ones instead, this would definitely bode ill for it.

Fortunately TBB provides a mechanism for backward compatibility (as it always does). TBB with exact exception propagation enabled does not apply the new semantics blindly. Before re-throwing an exception into the user code it checks the current task group context. Parallel algorithms in applications compiled with old compilers (and possibly old TBB versions) supply the context that says that tbb::captured_exception should be used.

Only when the application is compiled with a new compiler, will it request exact propagation semantics. And even in this case, if your code already relies on tbb::captured_exception, and you do not want or cannot change it, you can compile your app with TBB_USE_CAPTURED_EXCEPTION=1 macro to force old semantics.

At last, for applications under development, I would recommend to stack catch-blocks for exact exception classes (those of interest for you of course), following them with catch-block for tbb::captured_exception, and then with ellipsis catch-block (in case of any unexpected exception).

Instead of a conclusion, I'd like to note that the changes in exception handling are just a fraction of the waterfall of improvements made in TBB 2.2, and therefore you may want to skim through the full list of them or see the transitioning from 2.1 to 2.2 guidelines.

Para obter informações mais completas sobre otimizações do compilador, consulte nosso aviso de otimização.