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
124 lines (104 loc) · 3.06 KB
/
GC.jl
File metadata and controls
124 lines (104 loc) · 3.06 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
121
122
123
124
"""
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
# we have both this and `ENABLED` since there is no `isready(::Event)`
# for us to do a non-blocking check. Instead we must keep the event being triggered
# in-sync with `ENABLED[]`.
# We therefore modify both in `enable()` and `disable()` and nowhere else.
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