Skip to content

Commit

Permalink
Fix thread safety in atexit(f): Lock access to atexit_hooks (#16)
Browse files Browse the repository at this point in the history
- atexit(f) mutates global shared state.
- atexit(f) can be called anytime by any thread.
- Accesses & mutations to global shared state must be locked if they can be accessed from multiple threads.

Fixes #49746
  • Loading branch information
NHDaly authored May 19, 2023
1 parent 8b59441 commit 237a983
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 1 deletion.
3 changes: 2 additions & 1 deletion base/initdefs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ end
const atexit_hooks = Callable[
() -> Filesystem.temp_cleanup_purge(force=true)
]
const _atexit_hooks_lock = ReentrantLock()

"""
atexit(f)
Expand All @@ -363,7 +364,7 @@ calls `exit(n)`, then Julia will exit with the exit code corresponding to the
last called exit hook that calls `exit(n)`. (Because exit hooks are called in
LIFO order, "last called" is equivalent to "first registered".)
"""
atexit(f::Function) = (pushfirst!(atexit_hooks, f); nothing)
atexit(f::Function) = Base.@lock _atexit_hooks_lock (pushfirst!(atexit_hooks, f); nothing)

function _atexit()
while !isempty(atexit_hooks)
Expand Down
21 changes: 21 additions & 0 deletions test/threads_exec.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1062,3 +1062,24 @@ end
popfirst!(LOAD_PATH)
end
end

# issue #49746, thread safety in `atexit(f)`
@testset "atexit thread safety" begin
f = () -> nothing
before_len = length(Base.atexit_hooks)
@sync begin
for _ in 1:1_000_000
Threads.@spawn begin
atexit(f)
end
end
end
@test length(Base.atexit_hooks) == before_len + 1_000_000
@test all(hook -> hook === f, Base.atexit_hooks[1 : 1_000_000])

# cleanup
Base.@lock Base._atexit_hooks_lock begin
deleteat!(Base.atexit_hooks, 1:1_000_000)
end
end

0 comments on commit 237a983

Please sign in to comment.