diff --git a/base/initdefs.jl b/base/initdefs.jl index 4106ef4eb7777..f7a02291a8840 100644 --- a/base/initdefs.jl +++ b/base/initdefs.jl @@ -350,6 +350,7 @@ end const atexit_hooks = Callable[ () -> Filesystem.temp_cleanup_purge(force=true) ] +const _atexit_hooks_lock = ReentrantLock() """ atexit(f) @@ -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) diff --git a/test/threads_exec.jl b/test/threads_exec.jl index 9cd5992d90a74..52e45fa381ec2 100644 --- a/test/threads_exec.jl +++ b/test/threads_exec.jl @@ -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 +