Skip to content

MPR#7796: avoid running at_exit functions twice#1783

Closed
xavierleroy wants to merge 3 commits intoocaml:trunkfrom
xavierleroy:MPR7796
Closed

MPR#7796: avoid running at_exit functions twice#1783
xavierleroy wants to merge 3 commits intoocaml:trunkfrom
xavierleroy:MPR7796

Conversation

@xavierleroy
Copy link
Contributor

@xavierleroy xavierleroy commented May 17, 2018

In particular when the runtime is in cleanup-at-exit mode. Fixes MPR#7796

Other cases of multiple execution of at-exit functions remain, e.g. MPR#7253. See also #685.

In particular when the runtime is in cleanup-at-exit mode.
Other cases of multiple execution of at-exit functions remain,
e.g. MPR#7253.
@xavierleroy xavierleroy added this to the 4.07 milestone May 17, 2018
stdlib/stdlib.ml Outdated
let do_at_exit () =
(!exit_function) ();
(* MPR#7796: make sure these at-exit functions will not be run again *)
exit_function := (fun () -> ())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it happen that !exit_function runs at_exit several times and then do_at_exit again? In this case, with your definition, I think you will get repetitions, while

  let f = !exit_function in
  exit_function := (fun () -> ());
  f ()

does not.

@nojb
Copy link
Contributor

nojb commented May 17, 2018

One could also guard against repeated execution of the individual at-exit functions, as in:

diff --git a/stdlib/stdlib.ml b/stdlib/stdlib.ml
index f87787b9d4..d59bb8b170 100644
--- a/stdlib/stdlib.ml
+++ b/stdlib/stdlib.ml
@@ -538,6 +538,8 @@ let exit_function = ref flush_all
 
 let at_exit f =
   let g = !exit_function in
+  let already_ran = ref false in
+  let f () = if !already_ran then () else (already_ran := true; f ()) in
   exit_function := (fun () -> f(); g())
 
 let do_at_exit () = (!exit_function) ()

I think this should also fix the cases of repeated execution reported in MPR#7253.

@murmour
Copy link
Contributor

murmour commented May 17, 2018

Wow. A very good catch!

The PR is a clear improvement as it is, but @gasche's version (with erasing exit_function before calling it) is even better. This should also fix a few other buggy cases.

Nicer implementation of do_at_exit contributed by @gasche.
@xavierleroy
Copy link
Contributor Author

I don't think @gasche's scenario should happen in practice, but his alternate implementation is nicer, so I just commited it to this GPR.

@xclerc
Copy link
Contributor

xclerc commented May 17, 2018

Doesn't this second version create a race condition, if the change
is propagated to otherlibs/threads/stdlib.ml?

@damiendoligez
Copy link
Member

There are also race conditions in @xavierleroy's and @nojb's version, right?

@xclerc
Copy link
Contributor

xclerc commented May 17, 2018

I think the first version is not worse than what we had before w.r.t to
race condition: the registered function can be run more than once.
The second version seems to introduce a race condition causing a
registered function to be never called.

@murmour
Copy link
Contributor

murmour commented May 17, 2018

Replicating a few lines from Mutex (in order to avoid a compilation dependency) should help with the race conditions. By the way, wasn't there always a race condition in at_exit, if two threads happen to call it simultaneously?

@murmour
Copy link
Contributor

murmour commented May 17, 2018

Could a context switch happen when a closure is allocated?

let at_exit f =
  let g = !exit_function in
  exit_function := (fun () -> f(); g())

@mshinwell
Copy link
Contributor

Yes.

Update the otherlibs/threads version of the stdlib.
@xavierleroy
Copy link
Contributor Author

Thank you @xclerc for having obliquely reminded me to update the otherlibs/threads stdlib.

otherlibs/threads is the VM threads, where preemption occurs only at function calls and loops. So, reading a reference then assigning the reference is actually atomic, making the do_at_exit function thread-safe. (This is how mutexes are implemented in VM threads, actually.)

For system threads do_at_exit is not atomic and not thread safe, for reasons given by others above, but please please do keep in mind that the standard library is not thread safe to begin with. E.g. hash tables are not thread safe, and have never been, and that's a lot more serious than a race between exit and at_exit.

@xclerc
Copy link
Contributor

xclerc commented May 17, 2018

otherlibs/threads is the VM threads, where preemption occurs only at function calls and loops. So, reading a reference then assigning the reference is actually atomic, making the do_at_exit function thread-safe. (This is how mutexes are implemented in VM threads, actually.)

But, precisely, preemption could occur after reading and setting
exit_function when f is called. A second thread would then see
an empty exit_function and could terminate the program by calling
sys_exit. The registered function(s) would never be called in such
a scenario.

For system threads do_at_exit is not atomic and not thread safe, for reasons given by others above, but please please do keep in mind that the standard library is not thread safe to begin with. E.g. hash tables are not thread safe, and have never been, and that's a lot more serious than a race between exit and at_exit.

Sure; I guess my concern was with the composition of libraries.
When dealing with data structures, you can most of the time
shield yourself from a ill-behaved third-party library; it seems much
more difficult (if only possible) with features such as at_exit.
However, I reckon this is a corner case.

@xavierleroy
Copy link
Contributor Author

OK, I thought you were concerned about the (non-) atomicity of swapping the contents of the reference. Your scenario for a race condition can occur both with VM threads and system threads, I believe.

All this being said, I'd like to improve the sequential case (which this PR does) before worrying about thread safety. Can I please have permission to merge?

Copy link
Member

@gasche gasche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. I'll let @xavierleroy merge and cherry-pick.

@dra27
Copy link
Member

dra27 commented May 18, 2018

The potential for at_exit functions not to run at all in a threaded program I feel should at least be documented.

I agree with @xclerc that it does feel strange to introduce a thread safety issue in a feature where it's very difficult (indeed, impossible in the general case) to work around it in user code.

@xclerc
Copy link
Contributor

xclerc commented May 18, 2018

(It occurred to me that an alternative would be to only change
the documentation in order to require the functions registered
through the at_exit mechanism to be idempotent.)

@yakobowski
Copy link
Contributor

Having only idempotent functions registered through at_exit is not that convenient, since one common usage is to print out information when the program exits. If this solution (improving the documentation) ends up being chosen, it would be worth mentioning it is only required for concurrent code.

@xclerc
Copy link
Contributor

xclerc commented May 18, 2018

Well, the idea is just to put the burden on the user of at_exit.
If you have no threads and want to print something, just protect
the printing by a bool ref or use laziness.

@xavierleroy
Copy link
Contributor Author

Cent fois sur le métier... I'll be back with a different implementation.

@xavierleroy
Copy link
Contributor Author

I am back! See #1790. @nojb's suggestion above (#1783 (comment)) works great.

EmileTrotignon pushed a commit to EmileTrotignon/ocaml that referenced this pull request Jan 12, 2024
* add missing 'watch:' in watch scraper, less logging in planet scraper
* pin river to b990f702d29c7a7139920d701154e9db595eae1f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants