Make sure a function registered with at_exit is executed only once#1790
Make sure a function registered with at_exit is executed only once#1790xavierleroy merged 2 commits intoocaml:trunkfrom
at_exit is executed only once#1790Conversation
Fixes: MPR#7253, MPR#7796. As shown in the PRs above there are several cases where do_at_exit is called several times, causing functions registered with at_exit to be called several times. Also, an at_exit function that raises could prevent other at_exit functions from being run. This commit doesn't try to prevent multiple calls to do_at_exit, but makes sure that each function registered with at_exit is run at most once. The idea is due to Nicolás Ojeda Bär.
|
(For the record, I mentioned lazyness for use on client code |
|
Well, lazyness could have been used in the implementation of |
|
This seems to be a robust solution to the "double execution" problem. I see no issues with it. Since you're fixing One possible approach: let at_exit f =
let g = ref (fun () -> ()) in
let f_already_ran = ref false in
let f () =
if not !f_already_ran then (f_already_ran := true; f ());
!g ()
in
g := !exit_function;
exit_function := fThis is the silliest piece of code I wrote today, but it illustrates the idea. |
This problem is not fixed by this PR right ? |
|
@murmour, rour code can also be made more readable: let once f =
let has_run = ref false in
fun () ->
if !has_run then ()
else (has_run := true; f ())
let add_before f gref =
(* using (gref := fun () -> f (); !gref ()) would introduce
an infinite loop; instead we define old_g to be
the value of !gref before the assignment *)
let old_g = ref !gref in
let seq () = f (); !old_g () in
(* gref may be mutated concurrently during the allocation of seq,
so we made old_g a reference and update it afterwards. *)
old_g := !gref;
gref := seq
let at_exit f =
add_before (once f) exit_function |
|
May I suggest to use g_ref rather than gref?
|
Yes, totally. The change to avoid a race condition is unobvious. On the other hand, I find the original code (of this PR) very readable, with no need for improvements. Perhaps it'd suffice just to add a couple of comments? let at_exit f =
let g = ref (fun () -> ()) in (* a placeholder for !exit_function *)
let f_ran = ref false in
let f () =
if not !f_ran then (f_ran := true; f ());
!g ()
in
(* The following is thread-safe
(indirection in g was added to avoid a race condition) *)
g := !exit_function;
exit_function := f |
|
@dbuenzli asks:
Amusingly, the example you gave in MPR#7253 actually works, and is reproduced by the test I added. An exception is raised, then the Printexc code that reports this uncaught exception calls If you have several |
|
I intend to merge the code in its present state very soon, based on @murmour's positive review. Holler if you're not happy. As to making |
It won't hurt to be more atomic, yes. |
…1790) Fixes: MPR#7253 (in large part), MPR#7796 (in full). As shown in the PRs above there are several cases where do_at_exit is called several times, causing functions registered with at_exit to be called several times. Also, an at_exit function that raises could prevent other at_exit functions from being run. This commit doesn't try to prevent multiple calls to do_at_exit, but makes sure that each function registered with at_exit is run at most once. The idea is due to Nicolás Ojeda Bär.
|
Cherry-pick to 4.07: 6ce1e6d |
|
Even better: this pull request fixes MPR#7178 as well. |
As shown in MPR#7253 and MPR#7796, there are several cases where
do_at_exitis called several times, causing functions registered withat_exitto be called several times. Also, anat_exitfunction that raises could prevent otherat_exitfunctions from being run.This GPR makes sure that each function registered with
at_exitis run at most once. The idea and implementation is due to @nojb in #1783 (comment).Implementation notes:
exit, eachat_exitfunction will be called exactly once, but possibly not in the normal order.Lazymodule is not available inStdlib.