Intel® oneAPI Threading Building Blocks
Ask questions and share information about adding parallelism to your applications when using this threading library.

exception lifetime rules unclear

Steve_Nuchia
New Contributor I
1,412 Views
Yes, it's me with yet another queston about exceptions.

Several of us, all with quite a bit of experience but all new to TBB, have studied the available documentation on the TBB exception mechanism and we're all confused about lifetime management.

Did we miss something? Is there a succinct rule that we should memorize?

In order to retrofit parallel constructs into a legacy Windows GUI program I'm wrapping all the algorithms. Our code has historically caught certain exceptions above the layer I'm working at, cancelling a "task" but continuing the session. So the potential exists for an arbitrary number of exceptions to be thrown, so I really want to avoid creating a leak here.

Thanks,
-swn
0 Kudos
1 Solution
Andrey_Marochko
New Contributor III
1,374 Views
Quoting - Steve Nuchia
Something is still missing here. After calling my_2nd_copy->throw_self() I lose control. Is it the responsibility of the catch block to call destroy? If so, enforcing that throughout close to five million lines of code is going to be a challenge.
Well, TBB, as most of other more or less successfull (in terms of adoption) libraries, always is challenged with striking a good balance between usability (or ease-of-use) / efficiency (performance) / richness of functionality. As the popular sayng goes: pick up any two of them. In practice achieving this good balance usually means that you have to favor most frequent use cases that cover the largest part of the application field.

Your use case does not fall into this category :), but nevertheless it is supported, right? Of course completely automatic lifetime management would be great, but this means garbage collection. Obviously this is inacceptable solution for a compact library not tied to a particular compiler, runtime, or OS. You need to destroy the object pointed to by my_2nd_copy at any time after my_2nd_copy->throw_self() is invoked (including inside the catch block - because the catch block operates with the copy created by compiler). This can be done in any thread of the application.


View solution in original post

0 Kudos
22 Replies
Andrey_Marochko
New Contributor III
1,201 Views
As long as you throw exceptions as local objects (i.e. like "throw std::range_error();" ) and catch them by reference, the compiler will make sure that no leaks takes place even when you flood your application with exceptions. The lifetime of such exception objects is the catch-block that has caught it. I hope this is succinct enough :)

Even when you use "catch(...) {/*...*/}" the actual exception objects will be correctly destroyed. When you rethrow the caught exception using "throw;" operator its lifetime is extended into the next catch-block that intercepts it.

The only gotcha that may catch you in the future is the following change that will happen when you (and TBB) moves to VS2010. Currently any standard or custom exception thrown out of TBB task is replaced with and delivered into your catch-blocks as the tbb::captured_exception object. Starting with VS2010 we will be delivering the original exception object unchanged (C++0x supports such capability). But, you'll be able to preserve the current behavior by defining TBB_USE_CAPTURED_EXCEPTION macro.

0 Kudos
Steve_Nuchia
New Contributor I
1,201 Views

Obviously I didn't make my question clear enough. The information about the tbb movable exception stuff being obsoleted in VS2010 is good news, of course, but what are the rules for lifetime management of exceptions that have been intercepted and forwarded by TBB?

0 Kudos
Steve_Nuchia
New Contributor I
1,201 Views

Further clarification: there is a "destroy" virtual method defined for tbb exceptions but no clear rules are given for using it, its interaction with move and throw_self, etc.
0 Kudos
Steve_Nuchia
New Contributor I
1,201 Views

OK, I was hoping to get a definitive answer without disclosing exactly what I'm trying to accomplish. Appologies if anyone finds this offensive; it is in the best interest of my employer to be coy about things like this when possible. And since I work for them I am obliged to look out for their best interests.

The main thread in a Windows GUI app is special in a lot of ways. If you let it become a master then everywhere your single-threaded application did something that depends on being on the main thread, your converted body function now has to check which thread it is on. Yuck.

So I'm wrapping TBB with a "proxy thread" mechanism; calls to parallel algorithms on the main thread are intercepted, shipped to the proxy, and run there while the main thread runs a message-pumping loop. To complete the semantic emulation of the parallel algorithms I need to re-catch and re-move any exceptions forwarded out of the top-level task by TBB to my proxy, re-forwarding them to the main thread and re-throwing them there.

Writing the code to do this makes the lifetime management questions stand out. All I know at this point is that I don't know what the rules are. I tried to read the relevant code in the open source implementation but it is pretty convoluted and left me with no fewer questions than I started with.

0 Kudos
Andrey_Marochko
New Contributor III
1,201 Views
Doing wrappers over TBB is fine as long as it solves your architectural problems :).

Any exception that is thrown out of TBB task is intercepted inside the TBB scheduler and destroyed there as any normal C++ object. Thus as long as the exception class destructor cleans up all member data correctly, no leaks happen.

The first exception that was intercepted during the given algorithm run is copied, and the later exceptions are ignored (that is they are simply destroyed). The first exception also causes the algorithm to be cancelled and as soon as the pending tasks complete, the copy of the first exception is rethrown (so far in the form of either captured_exception or movable_exception). This copy is guaranteed to exist until the end of the catch-block that intercepts it, and then is automatically destroyed (whether it happens at the end of the catch-block or some time later is implementation defined, but normally it is the former).

If you want to rethrow this exception in the main (or any other) thread of the application, you need to create the second copy of this exception in the catch-block, something like:

[cpp]    tbb::tbb_exception *my_2nd_copy = NULL;
    . . .
    try {
        tbb::paralell_for( . . . );
    } catch ( tbb::tbb_exception& e ) {
        my_2nd_copy = e.move();
    }
[/cpp]

communicate my_2nd_copy to another thread (using a scheme of your own of course), and then rethrow it using the throw_self() method. In this case you are responsible for destroying my_2nd_copy later by means of the destroy() method.

0 Kudos
RafSchietekat
Valued Contributor III
1,201 Views
"In this case you are responsible for destroying my_2nd_copy later by means of the destroy() method."
Why destroy(), which I generally dislike? Couldn't the exception record whether it was "created by the move method", and act accordingly in the destructor? Or should I have spent a bit more time looking at the code before asking?
0 Kudos
Andrey_Marochko
New Contributor III
1,201 Views
Since the copy is created by the virtual "move" method, the paired virtual method is necessary to ensure the correctness of the destruction. Method "destroy" not only frees the memory (if necessary), it also invokes the destructor for "this" object. The scheme is essentially the same as with TBB tasks...
0 Kudos
RafSchietekat
Valued Contributor III
1,201 Views
Since the copy is created by the virtual "move" method, the paired virtual method is necessary to ensure the correctness of the destruction. Method "destroy" not only frees the memory (if necessary), it also invokes the destructor for "this" object. The scheme is essentially the same as with TBB tasks...

But having to call destroy() on a task is rare enough even for me not to fuss about it. :-) Why would it be a problem to give tbb_exception a virtual destructor instead of requiring users to deal with destroy()?
0 Kudos
Andrey_Marochko
New Contributor III
1,201 Views
Quoting - Raf Schietekat
But having to call destroy() on a task is rare enough even for me not to fuss about it. :-) Why would it be a problem to give tbb_exception a virtual destructor instead of requiring users to deal with destroy()?

Class tbb_exception does have virtual destructor (inherited from its base class std::exception). And normally, when it is thrown by TBB scheduler, you do not need to call destroy() explicitly. But in the use case described above the copy is manually created by means of the call to the move() method. This is actually dynamic allocation. As with any dynamic allocation you have to explicitly call free (or delete or whatever it is) method. Since move() is virtual and can be implemented differently you cannot just use C++ operator delete on the object creted in this way. Instead method destroy() is provided that both calls destructor and frees the memory.

With TBB tasks the situation is absolutely identical. You always use one of the factory allocate_* methods to create a task object. And the paired method destroy() have to be used eventually on each task object. However for every task that was spawned, its destroy method is invoked internally by the scheduler after the task was executed or canceled. This is why you do not have to do this manually most of the times.

0 Kudos
RafSchietekat
Valued Contributor III
1,201 Views
Sorry, that wasn't quite right. Revised question: wouldn't it be easier all around to just provide operator delete in captured_exception and movable_exception, or maybe even just in tbb_exception with my_dynamic refactored into the base class?
0 Kudos
Andrey_Marochko
New Contributor III
1,201 Views
Quoting - Raf Schietekat
Sorry, that wasn't quite right. Revised question: wouldn't it be easier all around to just provide operator delete in captured_exception and movable_exception, or maybe even just in tbb_exception with my_dynamic refactored into the base class?

Well, it is indeed possible. But the dominant design practice is to provide either a pair of member new and delete operators, or a pair of custom methods. The difference is not only stylistic. Member operators new and delete are always static, and thus can be inherited but not overridden in the derived classes. Thus our variant with virtual methods is generally more flexible. And from the stylistic point of view we did not need new semantics, because the user is not supposed to dynamically create TBB exception objects. What we needed was move semantics for the situations exactly like was described above (and also for internal purposes).

For TBB tasks, because the task allocation mechanics is not allowed to be overridden in the derived classes, it could've been possibly to use a bunch of overloaded member operators new and member operator delete. I'm not sure what were the exact reasons why TBB has what it has now. Probably it neither improved API expressiveness nor simplified the implementation. Maybe Aexey or Arch remember the motivation.

0 Kudos
RafSchietekat
Valued Contributor III
1,201 Views
"Member operators new and delete are always static, and thus can be inherited but not overridden in the derived classes."
They may be static (no "this"), but, with std::exception having a virtual destructor, "the deallocation function is the one found by the lookup in the definition of the dynamic type's virtual destructor". What more is required?

(Added) Note that my_dynamic apparently only serves to trap (if debugging) but then silently ignore inappropriate destroy() calls, so if there's no destroy() it is probably safe to trust the user not to call delete if not needed (seems like a basic survival skill in C++ land), so there's no need to remember it at operator delete time.
0 Kudos
Steve_Nuchia
New Contributor I
1,201 Views
Something is still missing here. After calling my_2nd_copy->throw_self() I lose control. Is it the responsibility of the catch block to call destroy? If so, enforcing that throughout close to five million lines of code is going to be a challenge.
0 Kudos
RafSchietekat
Valued Contributor III
1,201 Views
"Something is still missing here. After calling my_2nd_copy->throw_self() I lose control. Is it the responsibility of the catch block to call destroy? If so, enforcing that throughout close to five million lines of code is going to be a challenge."
As Andrey said (in #5): "In this case you are responsible for destroying my_2nd_copy later by means of the destroy() method." You could do that at any time after throw_self(): during stack unwinding, in the catch block iff you want to do it that way, or any time afterwards. Of course, if you could use delete instead of destroy(), you could just give my_2nd_copy to an auto_ptr on the stack. ;-) Well, you could always emulate that by having a shared_ptr's deleter call destroy(), but that seems a bit unwieldy.

(Removed irrelevant observation.)
0 Kudos
Andrey_Marochko
New Contributor III
1,375 Views
Quoting - Steve Nuchia
Something is still missing here. After calling my_2nd_copy->throw_self() I lose control. Is it the responsibility of the catch block to call destroy? If so, enforcing that throughout close to five million lines of code is going to be a challenge.
Well, TBB, as most of other more or less successfull (in terms of adoption) libraries, always is challenged with striking a good balance between usability (or ease-of-use) / efficiency (performance) / richness of functionality. As the popular sayng goes: pick up any two of them. In practice achieving this good balance usually means that you have to favor most frequent use cases that cover the largest part of the application field.

Your use case does not fall into this category :), but nevertheless it is supported, right? Of course completely automatic lifetime management would be great, but this means garbage collection. Obviously this is inacceptable solution for a compact library not tied to a particular compiler, runtime, or OS. You need to destroy the object pointed to by my_2nd_copy at any time after my_2nd_copy->throw_self() is invoked (including inside the catch block - because the catch block operates with the copy created by compiler). This can be done in any thread of the application.


0 Kudos
Andrey_Marochko
New Contributor III
1,201 Views
Quoting - Raf Schietekat
"Member operators new and delete are always static, and thus can be inherited but not overridden in the derived classes."
They may be static (no "this"), but, with std::exception having a virtual destructor, "the deallocation function is the one found by the lookup in the definition of the dynamic type's virtual destructor". What more is required?

(Added) Note that my_dynamic apparently only serves to trap (if debugging) but then silently ignore inappropriate destroy() calls, so if there's no destroy() it is probably safe to trust the user not to call delete if not needed (seems like a basic survival skill in C++ land), so there's no need to remember it at operator delete time.
Yes, you are right about correct operator delete selection. As it often happens the same functionality can be designed or implemented in different ways neither of which has a objective advantages over the others. I bet there are many people who prefer interface based approach to hard core C++ style. So the way it was implemented most probably reflects the tradition of TBB and the design preferences of its developers.

0 Kudos
RafSchietekat
Valued Contributor III
1,201 Views
"As it often happens the same functionality can be designed or implemented in different ways neither of which has a objective advantages over the others. I bet there are many people who prefer interface based approach to hard core C++ style."
See #14 for an objective advantage, for the user it's anything but hard core not to have to worry about delete vs. whatever else, and it even seems easier to implement... that's why I don't understand destroy().
0 Kudos
Andrey_Marochko
New Contributor III
1,201 Views
Quoting - Raf Schietekat
"As it often happens the same functionality can be designed or implemented in different ways neither of which has a objective advantages over the others. I bet there are many people who prefer interface based approach to hard core C++ style."
See #14 for an objective advantage, for the user it's anything but hard core not to have to worry about delete vs. whatever else, and it even seems easier to implement... that's why I don't understand destroy().

All right, the ability to use one of the existing smart pointers indeed seems to be a real convenience. Thanks to your insistence TBB API may get itself another extension :). We'll discuss the alternatives inside the team (the other alternative being TBB's own smart destroyer). Sounds good?

0 Kudos
RafSchietekat
Valued Contributor III
1,201 Views
"Sounds good?"
Sounds all right to me! Well, except... :-)
0 Kudos
Steve_Nuchia
New Contributor I
1,094 Views
I've designed many libraries, I understand the issues. There is no balance here -- this design is not usable in practice. Not correctly, not without imposing severe restrictions on the structure of the calling program.

To know what to do with the execption I have to know its history. If and only if it was moved before being thrown I MUST catch it using a tbb_exception reference catch signature and I MUST call destroy on it. If it was moved and it is caught by a ... block, it leaks. No saving throw. I could banish ... catch blocks if I had an extra lifetime to fix all the code and create unit tests that hit every throw in every library.

How do I enforce that? How do I even know it was moved? I guess I can enforce that any tbb_exception that gets thrown will have been moved, that helps a little. Unless somebody decides to throw one that hasn't been moved.

I could use a "smart pointer" style solution but the destructor for that will be called as the stack is unwinding, before the exception is seen by the final catch block. destroy-before-use is an anti-pattern, right? Pass it to a timed event and destroy it ten seconds in the future, just to be safe?

The only answer I can see here is to "unwrap" the original exception into a stack-based copy and rethrow that after calling destroy on the tbb_exception. That means I need code in my wrapper for every possible exception type in the program.

On the other hand, it means the rest of the program will see the exception type it was seeing before, rather than the wrapped type. Tradeoffs again.

I've come back to this old thread because I'm back to working on that code; I'd forgotten I started it but it still shows up near the top of the search results when you google "tbb_exception lifetime". Which I suppose supports the contention that mine is a corner case.

0 Kudos
Reply