Date: Fri, 10 Sep 1999 23:42:57 -0500
Subject: Asynchronous exceptions - possible? useful? implementable?
Newsgroups: comp.std.c++
The issue is asynchronous C++ exceptions. Problems with them, and uses. I start by considering the OS with multiple threads of control, to find problems with throwing an exception asynchronously. Then, I consider it from the point-of-view of a program having to handle an exception occurring anywhere. Finally, I look at practical uses of it. To finish it off, I show the implementation of the key elements. I can elaborate on the compiler implementation issue if necessary.
First, let's consider it from the standpoint of throwing an exception asynchronously, with regard to the OS and threads.
thread 1 // signal occurs here...
thread 2 // ... or maybe here
thread 3
Which thread does exception get thrown in? Is it acceptable to have it thrown in what is essentially a random thread?
thread scheduler
thread executing
thread scheduler // signal occurs here - "What do you do?!"
What if signal occurs when no thread is active? (assuming OS doesn't defer signal until a thread is executing)
To solve these, the thread scheduler could be told to throw an exception in a particular thread whenever it next becomes active.
template<typename T>
void throw_exception_in_thread( T const&, Thread_ID );
With the OS out of the way, consider a general problem:
int* temp = new int;
// asynchronous exception occurs here
delete temp;
What do we do here if an asynchronous exception is thrown at the point indicated? You might suggest a smart pointer, but that is just a distraction,
auto_ptr<int> temp( new int );
as the asynchronous exception could occur after the allocation takes place, but before the auto_ptr is constructed.
This could be worked around by providing a way to tell the thread scheduler not to throw any asynchronous exceptions for a particular region of code.
class no_throw_region {
// ...
};
{
no_throw_region protect;
int* temp = new int;
delete temp;
}
These regions could be nested.
Users would have to use safe objects at all times. Something like auto_ptr wouldn't cut it. We would need something that handled the allocation itself:
template<typename T>
class on_freestore {
T* obj;
public:
on_freestore() {
no_throw_region protect;
obj = new T;
}
template<typename A1
explicit on_freestore( A1 const& a1 ) {
no_throw_region protect;
obj = new T( a1 );
}
// more forwarding constructors
~on_freestore() {
delete obj;
}
};
Cases where this kind of protection couldn't be safely encapsulated would be quite common, requiring the user to explicitly protect regions of code.
This is essentially a critical section issue, where the piece of data that needs to be protected is the program counter itself! The usual critical section solutions apply. For example, if we had some type that didn't protect itself internally from asynchronous exceptions, we would have to explicitly protect calls:
Foo foo;
no_throw_region(), foo.f(); // region ends at end of statement
no_throw_region(), foo.g();
Constructors and destructors pose particular challenges. This could be handled by a proxy object that held storage for a Foo inside itself, and explicitly constructed and destructed the Foo in a protected manner:
class proxy {
aligned_storage<Foo> storage; // holds space for sizeof (T) object
public:
proxy() {
no_throw_region(), new (storage) Foo;
// storage implicitly converts to Foo*
}
~proxy() {
no_throw_region(), storage->Foo::~Foo();
}
Foo* operator -> () const { return storage; }
};
This isn't pretty. Perhaps the language could implicitly protect all constructors and destructors, since otherwise an exception could occur inside a destructor. This would be useful for the constructor because it can't easily be protected across the ctor initializer list and body. Just hope that the constructor isn't where a large amount of time is spent, because this would defer any exceptions.
With this general issue out of the way, we can move to language-specific issues.
What if an exception is already being handled in the thread? i.e.
void f() {
try {
throw 1;
}
catch ( ... ) {
// ...
}
}
execution:
try block
throw
-- processing exception
do cleanup of local objects
find matching catch handler
found
-- end exception processing
the exception occurs sometime inside the processing exception block.
The exception implementation could internally protect itself from easynchronous exceptions with a no-throw region.
I am quite sure all this could be implemented in the zero-overhead exception implementation without any overhead for the no-exception case. no-throw regions would be handled like other regions are (in the data tables).
I can't think of any reason offhand why a particular compiler couldn't implement this today. A program relying on it wouldn't be portable, of course.
Now, on to practical issues. The use of it, as I imagine, is to allow a signal to be propagated up the call chain, *cleaning up* things along the way. I have no problem with this, if it's useful.
The example often given is control-c:
struct abort_signal { };
Thread_ID main_thread_id;
void control_c_signal_handler() {
schedule_exception( main_thread_id, control_c() );
}
void do_long_processing() {
std::vector<int> data; // assume asynchronous exception safe
// operate on data
// write it out
}
void main_thread() {
try {
main_thread_id = cur_thread_id();
do_long_processing();
}
catch ( abort_signal ) {
// control-c pressed
}
}
OK.
This seems workable in my mind (including actual implementation).
I can imagine a bad case where the thread scheduler happens to always be invoked only when the code is in a no-throw section, so the control-c never gets handled. Note I'm assuming that a no-throw section doesn't check for pending exceptions on exit, because this could cause a severe performance problem as no-throw sections would likely be entered and exited too often. This could be fixed by adding a check that is executed often in the processing function:
void do_long_processing() {
// ...
handle_pending_exception();
}
This would be a function of the scheduler. It would do the same operation that is done whenever a thread is being re-scheduled.
But, we've almost come full-circle, and gained very little! handle_pending_exception() could just as well be implemented today in ISO C++ with no special language support. It would do everything described above (save exception and re-throw it later).
This check is how I've imagined the issue being handled all along. It is very simple to implement and understand.
I'm going to implement the above and get some practical data on issues that come up. Thinking can only go so far.
What practical uses does this have?
One I have already encountered before is terminating a thread - that is not a good idea in general, because it may have local state on its stack that needs to be cleaned up. Throwing an exception would be ideal here.
The other commonly-mentioned one is handling a termination request for an operation (control-C). Perhaps a long calculation is taking place. The calculation results can be in an incomplete state, so only the resources used for the calculation need to be protected (generally, the memory used for them).
It seems this is the general pattern, that of terminating something from a semi-arbitrary point. I can see that the exception solution could be much cleaner *and* more efficient than sprinking handle_pending_exception() calls everywhere (this would be like using a cooperative thread mechanism where you have to explicitly yield() to the scheduler).
Now, which side was I on again? Oh yeah, asynchronous exceptions are a bad idea!
(OK, so maybe I came to a different conclusion, at least as far as serious problems with it...)
Just for completeness, here is code to implement some of the mechanisms described above:
Holds an exception for later throwing. A specific case of a more general idiom of allowing an operation from a closed set of operations to be performed on an object of any type, determined purely at run-time, in a completely-typesafe manner.
class Thread_Exception_Holder
{
struct Exception {
virtual ~Exception() { }
void void throw_exception() = 0;
};
template<typename T>
struct Exception_t : Exception {
T const exception;
Exception_t( T const& t ) : exception( t ) { }
virtual void throw_exception() {
throw exception;
}
};
std::auto_ptr<Exception> exception;
public:
bool is_exception_pending() const {
return exception.get() != NULL;
}
void throw_exception() {
// cause exception to be deleted after it is thrown
std::auto_ptr<Exception> destroy_on_exit( exception );
destroy_on_exit->throw_exception();
}
struct already_pending { };
template<typename T>
void hold_exception( T const& t ) {
if ( exception.get() )
throw already_pending();
exception.reset( new Exception_t<T>( t ) );
}
};
OS thread mechanism
class Thread_ID {
// ...
};
Thread_ID cur_thread_id();
// assuming no compiler support for no_throw_region
std::map<Thread_ID,atomic_int> thread_no_throw_status;
bool is_in_no_throw_region() {
return thread_no_throw_status [cur_thread_id()] > 0;
}
class no_throw_region {
public:
no_throw_region() {
++thread_no_throw_status [cur_thread_id()];
}
~no_throw_region() {
--thread_no_throw_status [cur_thread_id()];
}
};
std::map<Thread_ID,Thread_Exception_Holder> thread_exceptions;
template<typename T>
void throw_exception_in_thread( Thread_ID, T const& exception ) {
thread_exceptions [id].hold_exception( exception );
}
void handle_pending_exception() {
ThreadID const cur( cur_thread_id() );
if ( hread_exceptions [cur].is_exception_pending() )
thread_exceptions [cur].throw_exception();
}
// assume this is implicitly invoked whenever a thread is
// to be rescheduled, and returns back to
// the thread
void thread_scheduler() {
// ...
// about to re-enter current thread
if ( !is_in_no_throw_region() )
handle_pending_exception();
}