Skip to content

Commit 237a983

Browse files
authored
Fix thread safety in atexit(f): Lock access to atexit_hooks (#16)
- 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
1 parent 8b59441 commit 237a983

File tree

2 files changed

+23
-1
lines changed

2 files changed

+23
-1
lines changed

base/initdefs.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ end
350350
const atexit_hooks = Callable[
351351
() -> Filesystem.temp_cleanup_purge(force=true)
352352
]
353+
const _atexit_hooks_lock = ReentrantLock()
353354

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

368369
function _atexit()
369370
while !isempty(atexit_hooks)

test/threads_exec.jl

+21
Original file line numberDiff line numberDiff line change
@@ -1062,3 +1062,24 @@ end
10621062
popfirst!(LOAD_PATH)
10631063
end
10641064
end
1065+
1066+
# issue #49746, thread safety in `atexit(f)`
1067+
@testset "atexit thread safety" begin
1068+
f = () -> nothing
1069+
before_len = length(Base.atexit_hooks)
1070+
@sync begin
1071+
for _ in 1:1_000_000
1072+
Threads.@spawn begin
1073+
atexit(f)
1074+
end
1075+
end
1076+
end
1077+
@test length(Base.atexit_hooks) == before_len + 1_000_000
1078+
@test all(hook -> hook === f, Base.atexit_hooks[1 : 1_000_000])
1079+
1080+
# cleanup
1081+
Base.@lock Base._atexit_hooks_lock begin
1082+
deleteat!(Base.atexit_hooks, 1:1_000_000)
1083+
end
1084+
end
1085+

0 commit comments

Comments
 (0)