Skip to content

Commit f7421f8

Browse files
author
Christopher Doris
committed
slightly more thread safe gc
1 parent 40fdbb9 commit f7421f8

File tree

3 files changed

+71
-18
lines changed

3 files changed

+71
-18
lines changed

src/C/pointers.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const CAPI_FUNC_SIGS = Dict{Symbol,Pair{Tuple,Type}}(
2222
:PyEval_RestoreThread => (Ptr{Cvoid},) => Cvoid,
2323
:PyGILState_Ensure => () => PyGILState_STATE,
2424
:PyGILState_Release => (PyGILState_STATE,) => Cvoid,
25+
:PyGILState_GetThisThreadState => () => Ptr{Cvoid},
26+
:PyGILState_Check => () => Cint,
2527
# IMPORT
2628
:PyImport_ImportModule => (Ptr{Cchar},) => PyPtr,
2729
:PyImport_Import => (PyPtr,) => PyPtr,

src/GC/GC.jl

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,36 @@ Like most PythonCall functions, you must only call this from the main thread.
3939
"""
4040
function enable()
4141
ENABLED[] = true
42-
if !isempty(QUEUE)
43-
C.with_gil(false) do
44-
for ptr in QUEUE
45-
if ptr != C.PyNULL
46-
C.Py_DecRef(ptr)
47-
end
48-
end
42+
if !isempty(QUEUE) && C.PyGILState_Check() == 1
43+
free_queue()
44+
end
45+
return
46+
end
47+
48+
function free_queue()
49+
for ptr in QUEUE
50+
if ptr != C.PyNULL
51+
C.Py_DecRef(ptr)
4952
end
5053
end
5154
empty!(QUEUE)
52-
return
55+
nothing
56+
end
57+
58+
function gc()
59+
if ENABLED[] && C.PyGILState_Check() == 1
60+
free_queue()
61+
true
62+
else
63+
false
64+
end
5365
end
5466

5567
function enqueue(ptr::C.PyPtr)
5668
if ptr != C.PyNULL && C.CTX.is_initialized
57-
if ENABLED[]
58-
C.with_gil(false) do
59-
C.Py_DecRef(ptr)
60-
end
69+
if ENABLED[] && C.PyGILState_Check() == 1
70+
C.Py_DecRef(ptr)
71+
isempty(QUEUE) || free_queue()
6172
else
6273
push!(QUEUE, ptr)
6374
end
@@ -67,19 +78,35 @@ end
6778

6879
function enqueue_all(ptrs)
6980
if C.CTX.is_initialized
70-
if ENABLED[]
71-
C.with_gil(false) do
72-
for ptr in ptrs
73-
if ptr != C.PyNULL
74-
C.Py_DecRef(ptr)
75-
end
81+
if ENABLED[] && C.PyGILState_Check() == 1
82+
for ptr in ptrs
83+
if ptr != C.PyNULL
84+
C.Py_DecRef(ptr)
7685
end
7786
end
87+
isempty(QUEUE) || free_queue()
7888
else
7989
append!(QUEUE, ptrs)
8090
end
8191
end
8292
return
8393
end
8494

95+
mutable struct GCHook
96+
function GCHook()
97+
finalizer(_gchook_finalizer, new())
98+
end
99+
end
100+
101+
function _gchook_finalizer(x)
102+
gc()
103+
finalizer(_gchook_finalizer, x)
104+
nothing
105+
end
106+
107+
function __init__()
108+
GCHook()
109+
nothing
110+
end
111+
85112
end # module GC

test/finalize_test_script.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using PythonCall
2+
3+
# This would consistently segfault pre-GC-thread-safety
4+
let
5+
pyobjs = map(pylist, 1:100)
6+
Threads.@threads for obj in pyobjs
7+
finalize(obj)
8+
end
9+
end
10+
11+
@show PythonCall.GC.ENABLED[]
12+
@show length(PythonCall.GC.QUEUE)
13+
GC.gc(false)
14+
# with GCHook, the queue should be empty now (the above gc() triggered GCHook to clear the PythonCall QUEUE)
15+
# without GCHook, gc() has no effect on the QUEUE
16+
@show length(PythonCall.GC.QUEUE)
17+
GC.gc(false)
18+
@show length(PythonCall.GC.QUEUE)
19+
GC.gc(false)
20+
@show length(PythonCall.GC.QUEUE)
21+
# with GCHook this is not necessary, GC.gc() is enough
22+
# without GCHook, this is required to free any objects in the PythonCall QUEUE
23+
PythonCall.GC.gc()
24+
@show length(PythonCall.GC.QUEUE)

0 commit comments

Comments
 (0)