forked from JuliaPy/PythonCall.jl
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGC.jl
More file actions
120 lines (100 loc) · 2.81 KB
/
GC.jl
File metadata and controls
120 lines (100 loc) · 2.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""
module PythonCall.GC
Garbage collection of Python objects.
See `disable` and `enable`.
"""
module GC
using ..C: C
# `ENABLED`: whether or not python GC is enabled, or paused to process later
const ENABLED = Threads.Atomic{Bool}(true)
# this event allows us to `wait` in a task until GC is re-enabled
const ENABLED_EVENT = Threads.Event()
# this is the queue to process pointers for GC (`C.Py_DecRef`)
const QUEUE = Channel{C.PyPtr}(Inf)
# this is the task which performs GC from thread 1
const GC_TASK = Ref{Task}()
# This we use in testing to know when our GC is running
const GC_FINISHED = Threads.Condition()
"""
PythonCall.GC.disable()
Disable the PythonCall garbage collector. This should generally not be required.
"""
function disable()
ENABLED[] = false
reset(ENABLED_EVENT)
return
end
"""
PythonCall.GC.enable()
Re-enable the PythonCall garbage collector. This should generally not be required.
"""
function enable()
ENABLED[] = true
notify(ENABLED_EVENT)
return
end
function enqueue(ptr::C.PyPtr)
if ptr != C.PyNULL && C.CTX.is_initialized
put!(QUEUE, ptr)
end
return
end
function enqueue_all(ptrs)
if C.CTX.is_initialized
for ptr in ptrs
put!(QUEUE, ptr)
end
end
return
end
# must only be called from thread 1 by the task in `GC_TASK[]`
function unsafe_process_queue!()
if !isempty(QUEUE)
C.with_gil(false) do
while !isempty(QUEUE) && ENABLED[]
# This should never block, since there should
# only be one consumer
# (we would like to not block while holding the GIL)
ptr = take!(QUEUE)
if ptr != C.PyNULL
C.Py_DecRef(ptr)
end
end
end
end
return nothing
end
function gc_loop()
while true
if ENABLED[] && !isempty(QUEUE)
unsafe_process_queue!()
# just for testing purposes
Base.@lock GC_FINISHED notify(GC_FINISHED)
end
# wait until there is both something to process
# and GC is `enabled`
wait(QUEUE)
wait(ENABLED_EVENT)
end
end
function launch_gc_task()
if isassigned(GC_TASK) && Base.istaskstarted(GC_TASK[]) && !Base.istaskdone(GC_TASK[])
throw(ConcurrencyViolationError("PythonCall GC task already running!"))
end
task = Task(gc_loop)
task.sticky = VERSION >= v"1.7" # disallow task migration which was introduced in 1.7
# ensure the task runs from thread 1
ccall(:jl_set_task_tid, Cvoid, (Any, Cint), task, 0)
schedule(task)
if isdefined(Base, :errormonitor)
Base.errormonitor(task)
end
GC_TASK[] = task
task
end
function __init__()
launch_gc_task()
enable() # start enabled
nothing
end
end # module GC