Topic: Asynchronous exceptions - possible? useful? implementable?
Author: postmast.root.admi.gov@iname.com (blargg)
Date: 1999/09/11 Raw View
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 same solutions
would 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();
The constructor and destructor 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 the 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, are 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 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. 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:
Thread_Exception_Holder() { }
bool is_exception_pending() const { return exception.get() != NULL; }
void throw_exception() {
exception->throw_exception();
exception.reset();
}
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 returning returns back to
// the thread
void thread_scheduler() {
// ...
// about to re-enter current thread
if ( !is_in_no_throw_region )
handle_pending_exception();
}
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html ]