Skip to content

Commit 943473f

Browse files
committed
Surround the GIL with a ReentrantLock on the Julia side.
The Python GIL should only be able obtained by a single Julia thread / task. To enforce this, we use a ReentrantLock on the Julia side that must be obtained before trying to acquire the Python GIL.
1 parent c84c059 commit 943473f

File tree

3 files changed

+79
-12
lines changed

3 files changed

+79
-12
lines changed

src/GIL/GIL.jl

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ module GIL
99

1010
using ..C: C
1111

12+
# Ensure that only one Julia thread tries to acquire the Python GIL
13+
# PyGILState_Ensure and PyGILState_Release may not be thread safe.
14+
# https://github.com/JuliaPy/PythonCall.jl/issues/627
15+
const _jl_gil_lock = ReentrantLock()
16+
17+
"""
18+
hasgil()
19+
20+
Returns `true` if the current thread has the GIL or `false` otherwise.
21+
"""
22+
hasgil() = C.PyGILState_Check() == Cint(1)
23+
1224
"""
1325
lock(f)
1426
@@ -21,11 +33,16 @@ threads. Since the main Julia thread holds the GIL by default, you will need to
2133
See [`@lock`](@ref) for the macro form.
2234
"""
2335
function lock(f)
24-
state = C.PyGILState_Ensure()
36+
Base.lock(_jl_gil_lock)
2537
try
26-
f()
38+
state = C.PyGILState_Ensure()
39+
try
40+
f()
41+
finally
42+
C.PyGILState_Release(state)
43+
end
2744
finally
28-
C.PyGILState_Release(state)
45+
Base.unlock(_jl_gil_lock)
2946
end
3047
end
3148

@@ -42,11 +59,16 @@ The macro equivalent of [`lock`](@ref).
4259
"""
4360
macro lock(expr)
4461
quote
45-
state = C.PyGILState_Ensure()
62+
Base.lock(_jl_gil_lock)
4663
try
47-
$(esc(expr))
64+
state = C.PyGILState_Ensure()
65+
try
66+
$(esc(expr))
67+
finally
68+
C.PyGILState_Release(state)
69+
end
4870
finally
49-
C.PyGILState_Release(state)
71+
Base.unlock(_jl_gil_lock)
5072
end
5173
end
5274
end
@@ -63,11 +85,17 @@ Python code. That other thread can be a Julia thread, which must lock the GIL us
6385
See [`@unlock`](@ref) for the macro form.
6486
"""
6587
function unlock(f)
66-
state = C.PyEval_SaveThread()
88+
_locked = Base.islocked(_jl_gil_lock)
89+
_locked && Base.unlock(_jl_gil_lock)
6790
try
68-
f()
91+
state = C.PyEval_SaveThread()
92+
try
93+
f()
94+
finally
95+
C.PyEval_RestoreThread(state)
96+
end
6997
finally
70-
C.PyEval_RestoreThread(state)
98+
_locked && Base.lock(_jl_gil_lock)
7199
end
72100
end
73101

@@ -84,13 +112,26 @@ The macro equivalent of [`unlock`](@ref).
84112
"""
85113
macro unlock(expr)
86114
quote
87-
state = C.PyEval_SaveThread()
115+
_locked = Base.islocked(_jl_gil_lock)
116+
_locked && Base.unlock(_jl_gil_lock)
88117
try
89-
$(esc(expr))
118+
state = C.PyEval_SaveThread()
119+
try
120+
$(esc(expr))
121+
finally
122+
C.PyEval_RestoreThread(state)
123+
end
90124
finally
91-
C.PyEval_RestoreThread(state)
125+
_locked && Base.lock(_jl_gil_lock)
92126
end
93127
end
94128
end
95129

130+
# If the main thread already has the GIL, we should lock _jl_gil_lock.
131+
function __init__()
132+
if hasgil()
133+
Base.lock(_jl_gil_lock)
134+
end
135+
end
136+
96137
end

test/GC.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using TestItemRunner
2+
13
@testitem "GC.gc()" begin
4+
using PythonCall
25
let
36
pyobjs = map(pylist, 1:100)
47
PythonCall.GIL.@unlock Threads.@threads for obj in pyobjs
@@ -13,6 +16,7 @@
1316
end
1417

1518
@testitem "GC.GCHook" begin
19+
using PythonCall
1620
let
1721
pyobjs = map(pylist, 1:100)
1822
PythonCall.GIL.@unlock Threads.@threads for obj in pyobjs
@@ -23,5 +27,10 @@ end
2327
VERSION >= v"1.10.0-" &&
2428
@test !isempty(PythonCall.GC.QUEUE.items)
2529
GC.gc()
30+
31+
# Unlock and relocking the ReentrantLock allows this test to pass
32+
Base.unlock(PythonCall.GIL._jl_gil_lock)
33+
Base.lock(PythonCall.GIL._jl_gil_lock)
34+
2635
@test isempty(PythonCall.GC.QUEUE.items)
2736
end

test/GIL.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818
if Threads.nthreads() 2
1919
@test 0.9 < t.time < 1.2
2020
end
21+
22+
@test PythonCall.GIL.hasgil()
23+
PythonCall.GIL.unlock() do
24+
@test !Base.islocked(PythonCall.GIL._jl_gil_lock)
25+
PythonCall.GIL.lock() do
26+
@test Base.islocked(PythonCall.GIL._jl_gil_lock)
27+
end
28+
end
2129
end
2230

2331
@testitem "@unlock and @lock" begin
@@ -36,4 +44,13 @@ end
3644
if Threads.nthreads() 2
3745
@test 0.9 < t.time < 1.2
3846
end
47+
48+
@test PythonCall.GIL.hasgil()
49+
PythonCall.GIL.@unlock begin
50+
@test !Base.islocked(PythonCall.GIL._jl_gil_lock)
51+
PythonCall.GIL.@lock begin
52+
@test Base.islocked(PythonCall.GIL._jl_gil_lock)
53+
end
54+
end
55+
3956
end

0 commit comments

Comments
 (0)