Discussion:
make exit() & friends thread-safe/reentrant
(too old to reply)
V***@ncl.ac.uk
2018-08-23 12:19:08 UTC
Permalink
From cplusplus.com: "If a program calls both exit and quick_exit, or
quick_exit more than once, it causes *undefined behaviour*."

(Nothing is said about exit() being non-re-entrant in
https://en.cppreference.com/w/cpp/utility/program/exit.)

(There was a somewhat relevant discussion in the "Calling std::exit and
object destruction" thread, but for a different reason.)

The worry is that there are many latent bugs at large due to this UB -
there is much sequential legacy code where error handling was to print a
message and exit with some return code. So suppose we have S1() and S2()
residing in independent libraries. That sequential code could now be called
concurrently in a multithreaded environment, by a programmer who does not
know the internals of the libraries. Since the libraries are independent,
any race conditions *seem* impossible, so why not? Even without legacy
code, it is so natural to call exit() if a more complicated error handling
is not warranted by the application - I'd say it's exit()'s purpose in
life, and it should fulfil it in the multithreaded program too.

The speculation that most library implementations of exit() have some sort
of a call-once flag seems untrue - I've been recently bitten by such a
bug (the program would occasionally hang instead of terminating; the bug
was sitting there quietly for years). So apparently Visual Studio
implementation of exit() is not re-entrant; g++ was handling this ok, so I
guess it manages races somehow. Furthermore, a naïve custom implementation
like my_exit(int n){ grab_mutex(); exit(n); } does not work because the
synchronisation object's handle may be destroyed by exit() while another
thread is waiting on it.

So, to prevent planes falling off the sky, why not make exit() & friends
(like quick_exit(), the normal exit from main(), etc.)
thread-safe/re-entrant? (Both in C and C++.) I really cannot see any
good reason for them not being such. There is of course a conflict which
error code to return - I'd say choose one non-deterministically.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Thiago Macieira
2018-08-23 15:00:04 UTC
Permalink
Post by V***@ncl.ac.uk
That sequential code could now be called
concurrently in a multithreaded environment, by a programmer who does not
know the internals of the libraries. Since the libraries are independent,
any race conditions *seem* impossible, so why not?
That assumption is faulty. Each function needs to declare that it is thread-
safe and/or reentrant, even if all it does is call certain C library
functions. That's because the C library has other, non-thread-safe functions,
like strtok(). If the library in question calls one of those, it's not thread-
safe and the application developer needs to know.

Your request has merit, but this justification does not. If two functions that
call thread-unsafe functions are run in different threads, the execution could
cause races and thus undefined behaviour. In this regard, exit() is not
special.

What makes it special is that exit() is almost never called, only in
exceptional circumstances. So the problem has already found abnormal
conditions if the library wants to call it. I'll grant you that one abnormal
condition could cause two threads to want ot call exit(), but the worst that
could happen is to make a bad situation worse.

Again, your proposal has merit, but it also has very small value. I'd
encourage you to write a paper declaring that it is undefined which of the two
threads actually performs the global destruction and whose return code is
passed to the environment, but all other threads simply hang forever. (Note:
"hang forever" is UB).
--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
V***@ncl.ac.uk
2018-08-23 15:39:08 UTC
Permalink
Post by Thiago Macieira
Post by V***@ncl.ac.uk
Since the libraries are independent,
any race conditions *seem* impossible, so why not?
That assumption is faulty. Each function needs to declare that it is thread-
safe and/or reentrant, even if all it does is call certain C library
functions. That's because the C library has other, non-thread-safe functions,
like strtok(). If the library in question calls one of those, it's not thread-
safe and the application developer needs to know.
Agreed - so need some extra text saying that the programmer has reasons to
believe strtok() & similar functions are not called. Another consideration
is that many C-libraries implementations substitute such functions by
macros and/or use thread-local storage (like errno), so they can be
implemented in a thread-safe way.
Post by Thiago Macieira
Your request has merit, but this justification does not.
Thanks, I guess the justification can be improved / expanded, as long as
people agree there is merit.
Post by Thiago Macieira
If two functions that
call thread-unsafe functions are run in different threads, the execution could
cause races and thus undefined behaviour. In this regard, exit() is not
special.
What makes it special is that exit() is almost never called, only in
exceptional circumstances. So the problem has already found abnormal
conditions if the library wants to call it. I'll grant you that one abnormal
condition could cause two threads to want ot call exit(), but the worst that
could happen is to make a bad situation worse.
That includes turning a minor hiccup to a catastrophe.

In my case the problem was in the back-end executable, and the front-end
would simply report it to the user (the error in most cases is caused by
the user's input). However, if back-end hangs, the front-end has to wait
forever. Not exactly a catastrophe, but you could imagine one.
Post by Thiago Macieira
it is undefined which of the two
threads actually performs the global destruction and whose return code is
passed to the environment,
Yes, but I think it's unnecessarily restrictive (e.g. there could be
concurrency in the clean-up code). I'd just say that exit() is
thread-safe/re-entrant, and the clean-up actions are performed only once.
Post by Thiago Macieira
but all other threads simply hang forever.
I'd not say that - the threads which lost the race should be eventually
destroyed when the process is destroyed.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Thiago Macieira
2018-08-24 04:12:34 UTC
Permalink
Post by V***@ncl.ac.uk
In my case the problem was in the back-end executable, and the front-end
would simply report it to the user (the error in most cases is caused by
the user's input). However, if back-end hangs, the front-end has to wait
forever. Not exactly a catastrophe, but you could imagine one.
I think you should pursue this as a paper.

But I also think you should refactor your own code not to use exit(). If
you're in an inconsistent state where recovery is not possible, you should
_exit(), _Exit(), quick_exit(), abort() or std::terminate(). Letting global
destructors run after something went wrong is ill-advised.

abort() is what a failled assert() does.
--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
V***@ncl.ac.uk
2018-08-24 09:31:22 UTC
Permalink
I think you should pursue this as a paper.
Ok, so far there have been no objections, so seems like a viable proposal.
Post by Thiago Macieira
But I also think you should refactor your own code not to use exit(). If
you're in an inconsistent state where recovery is not possible, you should
_exit(), _Exit(), quick_exit(), abort() or std::terminate(). Letting global
destructors run after something went wrong is ill-advised.
abort() is what a failled assert() does.
I did refactor my code to call my own function which than calls _Exit().
However, I was limited in what I could do because I link lots of legacy
code, which calls exit() and which I'd rather not touch.

My program didn't reach an inconsistent state, in most cases it was the
problem with the user input, so better recovery is theoretically possible,
but in practice it would be too much trouble and rather pointless. Hence I
want a nice exit, releasing system handles, closing temporary files, etc.,
and also I have to return an exit code for the front-end to report an
error. _Exit does the latter, but not the former, so not quite
satisfactory. Initially I tried to implement my own re-entrant exit using a
mutex, but this mutex can get destroyed while a thread is waiting on it.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
V***@ncl.ac.uk
2018-08-24 13:32:14 UTC
Permalink
Post by V***@ncl.ac.uk
Ok, so far there have been no objections, so seems like a viable proposal.
I've found a very much related issue that was raised before, and contacted
the author asking if he's willing to add that exit() & friends should be
thread-safe/re-entrant to it:
https://cplusplus.github.io/LWG/issue3084
(it occurred to me that I'd have to describe the interaction of exit
routines when they are called concurrently, so I feel I'm out of depth).
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Richard Smith
2018-08-24 20:03:05 UTC
Permalink
Post by Thiago Macieira
I think you should pursue this as a paper.
Ok, so far there have been no objections, so seems like a viable proposal.
Since no-one else has pointed this out...

This is a very dangerous thing to do; what you're talking about is the
analogue of throwing an exception out of a destructor while the stack is
already being unwound for another exception, and it has the same kinds of
problems. Just a taste: suppose we have:

thread_pool x;
thread y(x);

Now, during program shutdown, the idea is that the thread destructor runs
first and joins the thread, and then the thread_pool destructor runs and
cleans up some resources.

But joining the thread might fail in some indirect way, and some component
might decide the most reasonable thing to do is to call exit(). Since we've
already started destroying y, we presumably then immediately start
destroying x. But the thread controlled by y is still running, and cleaning
up the thread_pool's resources is likely to cause it to crash
(nondeterministically, of course).
Post by Thiago Macieira
But I also think you should refactor your own code not to use exit(). If
Post by Thiago Macieira
you're in an inconsistent state where recovery is not possible, you should
_exit(), _Exit(), quick_exit(), abort() or std::terminate(). Letting global
destructors run after something went wrong is ill-advised.
abort() is what a failled assert() does.
I did refactor my code to call my own function which than calls _Exit().
However, I was limited in what I could do because I link lots of legacy
code, which calls exit() and which I'd rather not touch.
My program didn't reach an inconsistent state, in most cases it was the
problem with the user input, so better recovery is theoretically possible,
but in practice it would be too much trouble and rather pointless. Hence I
want a nice exit, releasing system handles, closing temporary files, etc.,
and also I have to return an exit code for the front-end to report an
error. _Exit does the latter, but not the former, so not quite
satisfactory. Initially I tried to implement my own re-entrant exit using a
mutex, but this mutex can get destroyed while a thread is waiting on it.
If your OS is not incredibly incompetent, handles and files will be closed
as a consequence of terminating the program via terminate or abort, and a
suitable exit code will be produced.

If you're using legacy code that thinks it's appropriate to run your
program's atexit functions and global destructors at an arbitrary point in
your program's execution, you should seriously reconsider the decisions
that lead you to keep using that code.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
v***@gmail.com
2018-08-25 15:32:47 UTC
Permalink
Post by Richard Smith
This is a very dangerous thing to do;
Certainly, not more dangerous than the current (undefined) behaviour.
Making this UB defined (or at least more defined) would be a positive thing.
Post by Richard Smith
what you're talking about is the analogue of throwing an exception out of
a destructor while the stack is already being unwound for another
exception, and it has the same kinds of problems.
This is handled by terminate() - *much* better than UB. I think in case of
exit() one can always fall back to _Exit() if something bad happens.
Post by Richard Smith
thread_pool x;
thread y(x);
Now, during program shutdown, the idea is that the thread destructor runs
first and joins the thread, and then the thread_pool destructor runs and
cleans up some resources.
But joining the thread might fail in some indirect way, and some component
might decide the most reasonable thing to do is to call exit(). Since we've
already started destroying y, we presumably then immediately start
destroying x.
Well, destroying x would then clearly be unreasonable. If I understand you
correctly, exit() was called, and then called again from the destructor of
y? If exit is thread-safe and re-entrant, the second call is performed on
the same thread, and this can be detected (a reentrant mutex is needed),
and an appropriate action (e.g. _Exit) can be performed. The scenario you
describe is possible even in a sequential code.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
p***@lib.hu
2018-10-16 12:40:58 UTC
Permalink
exit() is problematic enough in a multi-thread environment.
Unless your other threads are designed to cooperate with exiting you have a
problem even by a single call to it. Whatever they are engaged to do. I see
little value to single out a second exit() call as part of the whatever and
treat in a special way.

How would you propose to implement it? I can think of an entry guard --
atomic_flag exit_started is set at the start.
so the first caller can proceed. And the rest do what?
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
v***@gmail.com
2018-10-16 13:18:04 UTC
Permalink
One would need something like a re-entrant lock. The first caller can
proceed, the others are blocked. If the first caller calls exit() again
(e.g. from some destructor of a static object or an atexit()-handler) then
this can be detected (using a flag), and then one can fall back at _Exit().
The lock itself need to be carefully destroyed at the very end, as there
could be threads waiting on it (e.g. just by calling _Exit()).

I think a more serious problem is when one thread is modifying a static
object and the other calls exit() that calls the destructor of that object.
I have no solution, but maybe there is some compiler (magic-statics) and OS
magic for that? exit() would need this magic anyway, so it would be nice if
an expert magician would give their opinion.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Continue reading on narkive:
Loading...