-
Notifications
You must be signed in to change notification settings - Fork 75
Surround the GIL with a ReentrantLock on the Julia side. #637
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
943473f
3b6caaa
beea5f2
2f795dc
53867da
58dba62
3b97f9c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,18 @@ module GIL | |
|
||
using ..C: C | ||
|
||
# Ensure that only one Julia thread tries to acquire the Python GIL | ||
# PyGILState_Ensure and PyGILState_Release may not be thread safe. | ||
# https://github.com/JuliaPy/PythonCall.jl/issues/627 | ||
const _jl_gil_lock = ReentrantLock() | ||
|
||
""" | ||
hasgil() | ||
|
||
Returns `true` if the current thread has the GIL or `false` otherwise. | ||
""" | ||
hasgil() = C.PyGILState_Check() == Cint(1) | ||
|
||
""" | ||
lock(f) | ||
|
||
|
@@ -21,11 +33,16 @@ threads. Since the main Julia thread holds the GIL by default, you will need to | |
See [`@lock`](@ref) for the macro form. | ||
""" | ||
function lock(f) | ||
state = C.PyGILState_Ensure() | ||
Base.lock(_jl_gil_lock) | ||
try | ||
f() | ||
state = C.PyGILState_Ensure() | ||
try | ||
f() | ||
finally | ||
C.PyGILState_Release(state) | ||
end | ||
finally | ||
C.PyGILState_Release(state) | ||
Base.unlock(_jl_gil_lock) | ||
end | ||
end | ||
|
||
|
@@ -42,11 +59,16 @@ The macro equivalent of [`lock`](@ref). | |
""" | ||
macro lock(expr) | ||
quote | ||
state = C.PyGILState_Ensure() | ||
Base.lock(_jl_gil_lock) | ||
try | ||
$(esc(expr)) | ||
state = C.PyGILState_Ensure() | ||
try | ||
$(esc(expr)) | ||
finally | ||
C.PyGILState_Release(state) | ||
end | ||
finally | ||
C.PyGILState_Release(state) | ||
Base.unlock(_jl_gil_lock) | ||
end | ||
end | ||
end | ||
|
@@ -63,11 +85,17 @@ Python code. That other thread can be a Julia thread, which must lock the GIL us | |
See [`@unlock`](@ref) for the macro form. | ||
""" | ||
function unlock(f) | ||
state = C.PyEval_SaveThread() | ||
_locked = Base.islocked(_jl_gil_lock) | ||
_locked && Base.unlock(_jl_gil_lock) | ||
try | ||
f() | ||
state = C.PyEval_SaveThread() | ||
try | ||
f() | ||
finally | ||
C.PyEval_RestoreThread(state) | ||
end | ||
finally | ||
C.PyEval_RestoreThread(state) | ||
_locked && Base.lock(_jl_gil_lock) | ||
end | ||
end | ||
|
||
|
@@ -84,13 +112,26 @@ The macro equivalent of [`unlock`](@ref). | |
""" | ||
macro unlock(expr) | ||
quote | ||
state = C.PyEval_SaveThread() | ||
_locked = Base.islocked(_jl_gil_lock) | ||
_locked && Base.unlock(_jl_gil_lock) | ||
try | ||
$(esc(expr)) | ||
state = C.PyEval_SaveThread() | ||
try | ||
$(esc(expr)) | ||
finally | ||
C.PyEval_RestoreThread(state) | ||
end | ||
finally | ||
C.PyEval_RestoreThread(state) | ||
_locked && Base.lock(_jl_gil_lock) | ||
end | ||
end | ||
end | ||
|
||
# If the main thread already has the GIL, we should lock _jl_gil_lock. | ||
function __init__() | ||
if hasgil() | ||
Base.lock(_jl_gil_lock) | ||
end | ||
end | ||
Comment on lines
+166
to
+170
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Locking on Rather we should consider invoking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disabling this apparently allowed the python tests to succeed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After sleeping on this, I propose you make the following changes to make this fix work:
Does that all sound logical and reasonable? My proposal already went through a few drafts as I tried to make it seem straightforward enough to explain and use, so it might not be the only way to deal with some of these constraints, and I'm probably forgetting at least some important details. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vtjnash Should I originally thought you meant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, Base.Condition, since this is locked to thread-local state |
||
|
||
end |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -1,4 +1,7 @@ | ||||||||||
using TestItemRunner | ||||||||||
|
||||||||||
@testitem "GC.gc()" begin | ||||||||||
using PythonCall | ||||||||||
let | ||||||||||
pyobjs = map(pylist, 1:100) | ||||||||||
PythonCall.GIL.@unlock Threads.@threads for obj in pyobjs | ||||||||||
|
@@ -13,6 +16,7 @@ | |||||||||
end | ||||||||||
|
||||||||||
@testitem "GC.GCHook" begin | ||||||||||
using PythonCall | ||||||||||
let | ||||||||||
pyobjs = map(pylist, 1:100) | ||||||||||
PythonCall.GIL.@unlock Threads.@threads for obj in pyobjs | ||||||||||
|
@@ -23,5 +27,10 @@ end | |||||||||
VERSION >= v"1.10.0-" && | ||||||||||
@test !isempty(PythonCall.GC.QUEUE.items) | ||||||||||
GC.gc() | ||||||||||
|
||||||||||
# Unlock and relocking the ReentrantLock allows this test to pass | ||||||||||
Base.unlock(PythonCall.GIL._jl_gil_lock) | ||||||||||
Base.lock(PythonCall.GIL._jl_gil_lock) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vtjnash If I remove this
Line 12 in c84c059
QUEUE.items is initialized as C.PyPtr[]
In this test, it should be emptied by this call in the finalizer of Lines 142 to 144 in c84c059
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. finalizers must be disabled when any locks are held to avoid interrupting a critical section and causing memory corruption |
||||||||||
|
||||||||||
@test isempty(PythonCall.GC.QUEUE.items) | ||||||||||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reasoning is nonsense since those functions are explicitly thread safe. There may be good reasons for this PR, but this is not it.