|
Boost : |
From: williamkempf_at_[hidden]
Date: 2001-11-15 10:41:15
--- In boost_at_y..., scleary_at_j... wrote:
> > From: williamkempf_at_h... [mailto:williamkempf_at_h...]
> >
> > > I'm using the term "exception-neutral" meaning that: for
library code
> that
> > > calls end-user functions, if those functions throw, the library
is still
> in
> > > a usable state.
>
> Sorry; my miscommunication! "exception-neutral" also implies that
any
> exceptions thrown by end-user functions are propogated up through
the
> library code.
OK. This doesn't change most of the analysis.
> You're technically correct. The POSIX standard does not include
anything
> about exceptions, AFAIK. I was just hoping that the definition of
> "cancellation" might cover exceptions, but probably not.
It simply can't, because POSIX is a C standard where there are no
such things as "exceptions".
> > Basically, the implementation must wrap the call in a try/catch
block
> > that does nothing and we must insure the "once_control" flag is
not
> > changed to indicate that the init_routine() was called if an
> > exception is thrown. Simple enough to do on all platforms. Now
> > let's look at what this means as far as behavior.
>
> This is a result of my miscommunication above. The behaviour I was
heading
> for was that if init_routine() threw, then call_once() would act as
though
> it had never been called (i.e., not set once_control), and then
propogate
> the exception.
>
> On the Win32 side, this can be done just by making the temporary
Mutex an
> actual object, so it is released + closed in its destructor.
There are several ways to implement this on all platforms, and it's
trivial everywhere. However, it doesn't change the analysis.
> > OK, let's assume that either boost::call_once returns an error or
> > that we manually set things up to be able to recognize the
failure of
> > init_routine().
>
> My intent is that the exception would indicate failure of
init_routine().
>
> > Now thread A can handle the error appropriately and
> > things seem OK. However, thread B in the mean time was blocking
in
> > it's own call to boost::call_once. Since the call in thread A is
> > supposed to behave as if it was never called this means that B
must
> > now call the init_routine() itself, but it's extremely unlikely
that
> > things have changed so that this call won't just throw as well.
So
> > the call to the init_routine() becomes wasted effort and most
would
> > consider it a bad thing that the call is made again (especially
if
> > you have N threads calling boost::call_once where N is a very
large
> > number and/or init_routine() is a lengthy/expensive routine that
> > nears completion before the exception is encountered).
>
> Most of the time, an init_routine() failure is indicitive of some
fatal
> error. Personally, I would just log/print/display the exception
message and
> exit. However, just in case someone else has a "non-critical
singleton",
> the ability to re-attempt construction exists.
There is a very large distinction between "log/print/display the
exception and exit" and what really happens here. User code can wrap
the call in a try/catch block and ignore the error, causing severe
problems here, as being discussed. Considering this a programmer
error isn't a valid idea. This puts too much burden on the
programmer. He must know that this case is unique and that the
program simply must exit (unless it's a very rare circumstance where
another thread might successfully call the init_routine() with out
throwing).
> > In general it's a design flaw to allow init_routine() to throw.
>
> As I said, I think it would be a fatal error -- in every case I can
think
> of, anyway. But not impossible, and not a design flaw if one of
your
> principles was "all error messages must get to the user".
You're looking at the problem wrong, I think. It is a design flaw
for init_routine() to throw. However, it's not a design flaw for
init_routine() to use a try/catch block to do precisely what you
want. If reporting the error and aborting is the most appropriate
action, which is likely the most prudent action in most cases, then
do this *IN* the init_routine() not after the call. This simplifies
the code tremendously, and insures there's no race conditions where
thread B could try to use shared data that failed to initialize (or
initialize the data a second time which because of side effects can
be even worse) before thread A catches and handles the exception that
was thrown.
On the other hand, if you don't want to abort but want all threads to
know that initialization failed so that they can handle the error
gracefully, then your init_routine() can use shared data to indicate
the failure of the initialization.
In other words, not being able to throw an exception out of
init_routine() doesn't restrict you from doing any for of error
handling you want to do. On the contrary, it insures you can handle
the error any way you wish.
The only way exceptions could be used would be if there were some way
to throw the same exception type thrown by init_routine() for all
threads calling boost::call_once() while only one thread is allowed
to call the init_routine(). This can be hand coded for specific
cases, but I know of no way to do this generically and portably.
> > Generally this just means
> > that you wrap the calls that can throw and set an error condition
in
> > some "global" state so that all threads know that the
initialization
> > failed but only the first call is executed.
>
> I was considering this, but it seems like a work-around for missing
> functionality. Maybe not, maybe this is just what I should do...
It's not missing functionality, IMHO, and I hope the above explains
it well enough that you agree.
> > > The reason: I'd like to allow singleton constructors to throw.
At least
> in
> > > my code, most of my singleton constructors can fail.
> >
> > That doesn't mean that the init_routine() that is called through
> > boost::call_once() and "constructs" the singleton needs to
throw.
> > Nor should it, IMHO.
>
> We need to get the error to the user -- either through exception
propogation
> (my idea) or through global data (your idea).
Exceptions can be thrown manually with only minor modifications to
the code.
boost::once_flag foo_once = BOOST_ONCE_INIT;
foo* pfoo=0;
bool foo_init_failed=false;
void init_foo()
{
try
{
static foo inst;
pfoo = &inst;
}
catch (...)
{
foo_init_failed = true;
}
}
foo& foo_instance()
{
boost::call_once(&init_foo, foo_once);
if (foo_init_failed)
throw some_exception_type();
return *pfoo;
}
Now all threads calling foo_instance() get an exception thrown when
initialization fails, but the initialization is only done once no
matter what. The trick is in knowing what exception to throw where
you have some_exception_type() above. It should be the same
exception thrown by the initialization code, but I don't know how
you'd do this across thread boundaries in a generic fashion. That's
why I don't think we're missing functionality.
> > > I think it might be useful to Boost.Threads -- but you might
want to
> just
> > > add it as a separate function, since its implementation may be
less
> > > efficient by-hand if we can't use pthread_once.
> >
> > I'm not convinced we need or should even consider this. Have I
> > swayed your thinking or am I missing something?
>
> It's your decision at this point. We can either write a call_once
that can
> propogate exceptions, or I can do the global data thing in the
singleton
> implementation. I prefer a propogating call_once, but it's your
call.
I can't make the call with out someone addressing the issues I've
layed out. If there's a solution that works I'm open to it.
Bill Kempf
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk