Skip to content

Commit 9aa4ff9

Browse files
Merge pull request #11 from c42f/cjf/threadsafety
Add global lock around the caching mechanism
2 parents 77a2637 + 0fab5cb commit 9aa4ff9

File tree

2 files changed

+56
-26
lines changed

2 files changed

+56
-26
lines changed

src/RuntimeGeneratedFunctions.jl

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -84,42 +84,53 @@ end
8484
# little non-robust to weird special cases like Main.eval being
8585
# Base.MainInclude.eval.)
8686

87+
# It appears we can't use a ReentrantLock here, as contention seems to lead to
88+
# deadlock. Perhaps because it triggers a task switch while compiling the
89+
# @generated function.
90+
_cache_lock = Threads.SpinLock()
8791
_cachename = Symbol("#_RuntimeGeneratedFunctions_cache")
8892
_tagname = Symbol("#_RuntimeGeneratedFunctions_ModTag")
8993

9094
function _cache_body(moduletag, id, body)
91-
cache = getfield(parentmodule(moduletag), _cachename)
92-
# Caching is tricky when `id` is the same for different AST instances:
93-
#
94-
# Tricky case #1: If a function body with the same `id` was cached
95-
# previously, we need to use that older instance of the body AST as the
96-
# canonical one rather than `body`. This ensures the lifetime of the
97-
# body in the cache will always cover the lifetime of the parent
98-
# `RuntimeGeneratedFunction`s when they share the same `id`.
99-
#
100-
# Tricky case #2: Unless we hold a separate reference to
101-
# `cache[id].value`, the GC can collect it (causing it to become
102-
# `nothing`). So root it in a local variable first.
103-
#
104-
cached_body = haskey(cache, id) ? cache[id].value : nothing
105-
cached_body = cached_body !== nothing ? cached_body : body
106-
# Use a WeakRef to allow `body` to be garbage collected. (After GC, the
107-
# cache will still contain an empty entry with key `id`.)
108-
cache[id] = WeakRef(cached_body)
109-
return cached_body
95+
lock(_cache_lock) do
96+
cache = getfield(parentmodule(moduletag), _cachename)
97+
# Caching is tricky when `id` is the same for different AST instances:
98+
#
99+
# Tricky case #1: If a function body with the same `id` was cached
100+
# previously, we need to use that older instance of the body AST as the
101+
# canonical one rather than `body`. This ensures the lifetime of the
102+
# body in the cache will always cover the lifetime of the parent
103+
# `RuntimeGeneratedFunction`s when they share the same `id`.
104+
#
105+
# Tricky case #2: Unless we hold a separate reference to
106+
# `cache[id].value`, the GC can collect it (causing it to become
107+
# `nothing`). So root it in a local variable first.
108+
#
109+
cached_body = haskey(cache, id) ? cache[id].value : nothing
110+
cached_body = cached_body !== nothing ? cached_body : body
111+
# Use a WeakRef to allow `body` to be garbage collected. (After GC, the
112+
# cache will still contain an empty entry with key `id`.)
113+
cache[id] = WeakRef(cached_body)
114+
return cached_body
115+
end
110116
end
111117

112118
function _lookup_body(moduletag, id)
113-
getfield(parentmodule(moduletag), _cachename)[id].value
119+
lock(_cache_lock) do
120+
cache = getfield(parentmodule(moduletag), _cachename)
121+
cache[id].value
122+
end
114123
end
115124

116125
function _ensure_cache_exists!(mod)
117-
if !isdefined(mod, _cachename)
118-
mod.eval(quote
119-
const $_cachename = Dict()
120-
struct $_tagname
121-
end
122-
end)
126+
lock(_cache_lock) do
127+
if !isdefined(mod, _cachename)
128+
mod.eval(quote
129+
const $_cachename = Dict()
130+
struct $_tagname
131+
end
132+
end)
133+
end
123134
end
124135
end
125136

test/runtests.jl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,22 @@ let
8888
end
8989
GC.gc()
9090
@test f_gc(1,-1) == 100001
91+
92+
# Test that threaded use works
93+
tasks = []
94+
for k=1:4
95+
let k=k
96+
t = Threads.@spawn begin
97+
r = Bool[]
98+
for i=1:100
99+
f = @RuntimeGeneratedFunction(Base.remove_linenums!(:((x,y)->x+y+$i*$k)))
100+
x = 1; y = 2;
101+
push!(r, f(x,y) == x + y + i*k)
102+
end
103+
r
104+
end
105+
push!(tasks, t)
106+
end
107+
end
108+
@test all(all.(fetch.(tasks)))
109+

0 commit comments

Comments
 (0)