-
Notifications
You must be signed in to change notification settings - Fork 77
More thread-safe GC #529
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
More thread-safe GC #529
Changes from 7 commits
f7421f8
3bcd028
1c516d6
8ca05c9
e230ce9
a36d7c0
a5a2c96
f021072
4b3bd65
56aa9bc
4fdcf31
9051769
13cc346
45bc71f
4ec7def
eb6b9f0
a68015e
ab560ac
cd4db5c
31cd57d
73f7eb8
2a54ca9
ca64d21
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 |
---|---|---|
|
@@ -3,77 +3,135 @@ | |
Garbage collection of Python objects. | ||
See `disable` and `enable`. | ||
See [`enable`](@ref), [`disable`](@ref) and [`gc`](@ref). | ||
""" | ||
module GC | ||
|
||
using ..C: C | ||
|
||
const ENABLED = Ref(true) | ||
const QUEUE = C.PyPtr[] | ||
const QUEUE_LOCK = Threads.SpinLock() | ||
cjdoris marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
const HOOK = WeakRef() | ||
|
||
""" | ||
PythonCall.GC.disable() | ||
Disable the PythonCall garbage collector. | ||
Do nothing. | ||
This means that whenever a Python object owned by Julia is finalized, it is not immediately | ||
freed but is instead added to a queue of objects to free later when `enable()` is called. | ||
!!! note | ||
Like most PythonCall functions, you must only call this from the main thread. | ||
Historically this would disable the PythonCall garbage collector. This was required | ||
for safety in multi-threaded code but is no longer needed, so this is now a no-op. | ||
""" | ||
function disable() | ||
ENABLED[] = false | ||
return | ||
end | ||
disable() = nothing | ||
cjdoris marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
cjdoris marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
""" | ||
PythonCall.GC.enable() | ||
Re-enable the PythonCall garbage collector. | ||
Do nothing. | ||
This frees any Python objects which were finalized while the GC was disabled, and allows | ||
objects finalized in the future to be freed immediately. | ||
!!! note | ||
Like most PythonCall functions, you must only call this from the main thread. | ||
Historically this would enable the PythonCall garbage collector. This was required | ||
for safety in multi-threaded code but is no longer needed, so this is now a no-op. | ||
""" | ||
function enable() | ||
ENABLED[] = true | ||
if !isempty(QUEUE) | ||
for ptr in QUEUE | ||
if ptr != C.PyNULL | ||
C.Py_DecRef(ptr) | ||
end | ||
enable() = nothing | ||
cjdoris marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
cjdoris marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
""" | ||
PythonCall.GC.gc() | ||
Free any Python objects waiting to be freed. | ||
These are objects that were finalized from a thread that was not holding the Python | ||
GIL at the time. | ||
Like most PythonCall functions, this must only be called from the main thread (i.e. the | ||
thread currently holding the Python GIL.) | ||
""" | ||
function gc() | ||
if C.CTX.is_initialized | ||
unsafe_free_queue() | ||
end | ||
nothing | ||
end | ||
|
||
function unsafe_free_queue() | ||
cjdoris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
lock(QUEUE_LOCK) | ||
|
||
for ptr in QUEUE | ||
if ptr != C.PyNULL | ||
C.Py_DecRef(ptr) | ||
end | ||
end | ||
empty!(QUEUE) | ||
return | ||
unlock(QUEUE_LOCK) | ||
nothing | ||
end | ||
|
||
function enqueue(ptr::C.PyPtr) | ||
if ptr != C.PyNULL && C.CTX.is_initialized | ||
if ENABLED[] | ||
if C.PyGILState_Check() == 1 | ||
C.Py_DecRef(ptr) | ||
if !isempty(QUEUE) | ||
unsafe_free_queue() | ||
end | ||
else | ||
lock(QUEUE_LOCK) | ||
push!(QUEUE, ptr) | ||
unlock(QUEUE_LOCK) | ||
cjdoris marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
end | ||
end | ||
return | ||
nothing | ||
end | ||
|
||
function enqueue_all(ptrs) | ||
if C.CTX.is_initialized | ||
if ENABLED[] | ||
if any(ptr -> ptr != C.PYNULL, ptrs) && C.CTX.is_initialized | ||
cjdoris marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
if C.PyGILState_Check() == 1 | ||
for ptr in ptrs | ||
if ptr != C.PyNULL | ||
C.Py_DecRef(ptr) | ||
end | ||
end | ||
if !isempty(QUEUE) | ||
unsafe_free_queue() | ||
end | ||
else | ||
lock(QUEUE_LOCK) | ||
|
||
append!(QUEUE, ptrs) | ||
unlock(QUEUE_LOCK) | ||
end | ||
end | ||
return | ||
nothing | ||
end | ||
|
||
""" | ||
GCHook() | ||
An immortal object which frees any pending Python objects when Julia's GC runs. | ||
cjdoris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This works by creating it but not holding any strong reference to it, so it is eligible | ||
to be finalized by Julia's GC. The finalizer empties the PythonCall GC queue if | ||
possible. The finalizer also re-attaches itself, so the object does not actually get | ||
collected and so the finalizer will run again at next GC. | ||
""" | ||
mutable struct GCHook | ||
cjdoris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
function GCHook() | ||
finalizer(_gchook_finalizer, new()) | ||
end | ||
end | ||
|
||
function _gchook_finalizer(x) | ||
if C.CTX.is_initialized | ||
finalizer(_gchook_finalizer, x) | ||
if !isempty(QUEUE) && C.PyGILState_Check() == 1 | ||
unsafe_free_queue() | ||
end | ||
end | ||
nothing | ||
end | ||
|
||
function __init__() | ||
HOOK.value = GCHook() | ||
cjdoris marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
nothing | ||
end | ||
|
||
end # module GC |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
using PythonCall | ||
|
||
# This would consistently segfault pre-GC-thread-safety | ||
let | ||
pyobjs = map(pylist, 1:100) | ||
Threads.@threads for obj in pyobjs | ||
finalize(obj) | ||
end | ||
end |
Uh oh!
There was an error while loading. Please reload this page.