Skip to content

Commit 49be855

Browse files
committed
Wrap GIL acquire / release in ReentrantLock
Required to avoid starving the scheduler or deadlocking against the GC.
1 parent 25038c7 commit 49be855

File tree

2 files changed

+52
-0
lines changed

2 files changed

+52
-0
lines changed

src/gil.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,28 @@
22

33
const _GIL_owner = Threads.Atomic{UInt}(0)
44

5+
# It is not legal for Julia code to spin directly on "foreign" locks such as
6+
# the GIL, since this can mean never making progress toward:
7+
#
8+
# * GC safepoints, which causes deadlocks when other threads initiate a GC
9+
# and wait forever for all threads to "stop".
10+
#
11+
# * yield() points, which can starve the scheduler and also lead to deadlocks
12+
# since Tasks waiting for the GIL can block other Tasks ready to make
13+
# progress from being scheduled.
14+
#
15+
# Instead we are required to hold a Julia-side lock that is GC-aware + scheduler-
16+
# aware whenever we hold the foreign GIL, preventing these deadlocks.
17+
const _jl_gil = ReentrantLock()
18+
519
# Forward declare deferred destructors
620
function _defer_Py_DecRef end
721
function _defer_PyBuffer_Release end
822

923
# Acquires the GIL.
1024
# This lock can be re-acquired if already held by the OS thread (it is re-entrant).
1125
function GIL_lock()
26+
lock(_jl_gil)
1227
state = ccall((@pysym :PyGILState_Ensure), Cint, ())
1328
_GIL_owner[] = objectid(current_task())
1429
return state
@@ -19,6 +34,7 @@ end
1934
function GIL_unlock(state::Cint)
2035
_GIL_owner[] = UInt(0)
2136
ccall((@pysym :PyGILState_Release), Cvoid, (Cint,), state)
37+
unlock(_jl_gil)
2238
end
2339

2440
# Quickly check whether this task holds the GIL.

test/gil.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,40 @@ using Base.Threads
3838
@test isempty(PyCall._deferred_Py_DecRef)
3939
end
4040

41+
@testset "GIL + GC safepoint deadlock" begin
42+
done = Threads.Atomic{Bool}(false)
43+
44+
# If not protected by a (GC-aware) Julia-level lock, it is possible
45+
# to deadlock with the GC + GIL:
46+
# 1. Task A holds the GIL
47+
# 2. Task B waits to access the GIL
48+
# 3. Task A triggers GC
49+
# 4. Task B never reaches a safepoint, due to waiting for the GIL.
50+
task1 = Threads.@spawn begin
51+
while !done[]
52+
PyCall.@with_GIL begin
53+
for _ in 1:10_000
54+
PyObject(0)
55+
end
56+
end
57+
end
58+
end
59+
60+
task2 = Threads.@spawn begin
61+
while !done[]
62+
PyCall.@with_GIL begin
63+
for _ in 1:10_000
64+
PyObject(0)
65+
end
66+
end
67+
GC.gc(true)
68+
end
69+
end
70+
71+
result = timedwait(() -> istaskdone(task1) || istaskdone(task2), 3.0)
72+
done[] = true
73+
timedwait(() -> istaskdone(task1) && istaskdone(task2), 5.0)
74+
@test result === :timed_out
75+
end
76+
4177
end

0 commit comments

Comments
 (0)