|  | 
| 3 | 3 | 
 | 
| 4 | 4 | Garbage collection of Python objects. | 
| 5 | 5 | 
 | 
| 6 |  | -See `disable` and `enable`. | 
|  | 6 | +See [`gc`](@ref). | 
| 7 | 7 | """ | 
| 8 | 8 | module GC | 
| 9 | 9 | 
 | 
| 10 | 10 | using ..C: C | 
| 11 | 11 | 
 | 
| 12 |  | -const ENABLED = Ref(true) | 
| 13 |  | -const QUEUE = C.PyPtr[] | 
|  | 12 | +const QUEUE = (; items = C.PyPtr[], lock = Threads.SpinLock()) | 
|  | 13 | +const HOOK = Ref{WeakRef}() | 
| 14 | 14 | 
 | 
| 15 | 15 | """ | 
| 16 | 16 |     PythonCall.GC.disable() | 
| 17 | 17 | 
 | 
| 18 |  | -Disable the PythonCall garbage collector. | 
|  | 18 | +Do nothing. | 
| 19 | 19 | 
 | 
| 20 |  | -This means that whenever a Python object owned by Julia is finalized, it is not immediately | 
| 21 |  | -freed but is instead added to a queue of objects to free later when `enable()` is called. | 
|  | 20 | +!!! note | 
| 22 | 21 | 
 | 
| 23 |  | -Like most PythonCall functions, you must only call this from the main thread. | 
|  | 22 | +    Historically this would disable the PythonCall garbage collector. This was required | 
|  | 23 | +    for safety in multi-threaded code but is no longer needed, so this is now a no-op. | 
| 24 | 24 | """ | 
| 25 | 25 | function disable() | 
| 26 |  | -    ENABLED[] = false | 
| 27 |  | -    return | 
|  | 26 | +    Base.depwarn( | 
|  | 27 | +        "disabling the PythonCall GC is no longer needed for thread-safety", | 
|  | 28 | +        :disable, | 
|  | 29 | +    ) | 
|  | 30 | +    nothing | 
| 28 | 31 | end | 
| 29 | 32 | 
 | 
| 30 | 33 | """ | 
| 31 | 34 |     PythonCall.GC.enable() | 
| 32 | 35 | 
 | 
| 33 |  | -Re-enable the PythonCall garbage collector. | 
|  | 36 | +Do nothing. | 
| 34 | 37 | 
 | 
| 35 |  | -This frees any Python objects which were finalized while the GC was disabled, and allows | 
| 36 |  | -objects finalized in the future to be freed immediately. | 
|  | 38 | +!!! note | 
| 37 | 39 | 
 | 
| 38 |  | -Like most PythonCall functions, you must only call this from the main thread. | 
|  | 40 | +    Historically this would enable the PythonCall garbage collector. This was required | 
|  | 41 | +    for safety in multi-threaded code but is no longer needed, so this is now a no-op. | 
| 39 | 42 | """ | 
| 40 | 43 | function enable() | 
| 41 |  | -    ENABLED[] = true | 
| 42 |  | -    if !isempty(QUEUE) | 
| 43 |  | -        for ptr in QUEUE | 
|  | 44 | +    Base.depwarn( | 
|  | 45 | +        "disabling the PythonCall GC is no longer needed for thread-safety", | 
|  | 46 | +        :enable, | 
|  | 47 | +    ) | 
|  | 48 | +    nothing | 
|  | 49 | +end | 
|  | 50 | + | 
|  | 51 | +""" | 
|  | 52 | +    PythonCall.GC.gc() | 
|  | 53 | +
 | 
|  | 54 | +Free any Python objects waiting to be freed. | 
|  | 55 | +
 | 
|  | 56 | +These are objects that were finalized from a thread that was not holding the Python | 
|  | 57 | +GIL at the time. | 
|  | 58 | +
 | 
|  | 59 | +Like most PythonCall functions, this must only be called from the main thread (i.e. the | 
|  | 60 | +thread currently holding the Python GIL.) | 
|  | 61 | +""" | 
|  | 62 | +function gc() | 
|  | 63 | +    if C.CTX.is_initialized | 
|  | 64 | +        unsafe_free_queue() | 
|  | 65 | +    end | 
|  | 66 | +    nothing | 
|  | 67 | +end | 
|  | 68 | + | 
|  | 69 | +function unsafe_free_queue() | 
|  | 70 | +    Base.@lock QUEUE.lock begin | 
|  | 71 | +        for ptr in QUEUE.items | 
| 44 | 72 |             if ptr != C.PyNULL | 
| 45 | 73 |                 C.Py_DecRef(ptr) | 
| 46 | 74 |             end | 
| 47 | 75 |         end | 
|  | 76 | +        empty!(QUEUE.items) | 
| 48 | 77 |     end | 
| 49 |  | -    empty!(QUEUE) | 
| 50 |  | -    return | 
|  | 78 | +    nothing | 
| 51 | 79 | end | 
| 52 | 80 | 
 | 
| 53 | 81 | function enqueue(ptr::C.PyPtr) | 
|  | 82 | +    # If the ptr is NULL there is nothing to free. | 
|  | 83 | +    # If C.CTX.is_initialized is false then the Python interpreter hasn't started yet | 
|  | 84 | +    # or has been finalized; either way attempting to free will cause an error. | 
| 54 | 85 |     if ptr != C.PyNULL && C.CTX.is_initialized | 
| 55 |  | -        if ENABLED[] | 
|  | 86 | +        if C.PyGILState_Check() == 1 | 
|  | 87 | +            # If the current thread holds the GIL, then we can immediately free. | 
| 56 | 88 |             C.Py_DecRef(ptr) | 
|  | 89 | +            # We may as well also free any other enqueued objects. | 
|  | 90 | +            if !isempty(QUEUE.items) | 
|  | 91 | +                unsafe_free_queue() | 
|  | 92 | +            end | 
| 57 | 93 |         else | 
| 58 |  | -            push!(QUEUE, ptr) | 
|  | 94 | +            # Otherwise we push the pointer onto the queue to be freed later, either: | 
|  | 95 | +            # (a) If a future Python object is finalized on the thread holding the GIL | 
|  | 96 | +            #     in the branch above. | 
|  | 97 | +            # (b) If the GCHook() object below is finalized in an ordinary GC. | 
|  | 98 | +            # (c) If the user calls PythonCall.GC.gc(). | 
|  | 99 | +            Base.@lock QUEUE.lock push!(QUEUE.items, ptr) | 
| 59 | 100 |         end | 
| 60 | 101 |     end | 
| 61 |  | -    return | 
|  | 102 | +    nothing | 
| 62 | 103 | end | 
| 63 | 104 | 
 | 
| 64 | 105 | function enqueue_all(ptrs) | 
| 65 |  | -    if C.CTX.is_initialized | 
| 66 |  | -        if ENABLED[] | 
|  | 106 | +    if any(!=(C.PYNULL), ptrs) && C.CTX.is_initialized | 
|  | 107 | +        if C.PyGILState_Check() == 1 | 
| 67 | 108 |             for ptr in ptrs | 
| 68 | 109 |                 if ptr != C.PyNULL | 
| 69 | 110 |                     C.Py_DecRef(ptr) | 
| 70 | 111 |                 end | 
| 71 | 112 |             end | 
|  | 113 | +            if !isempty(QUEUE.items) | 
|  | 114 | +                unsafe_free_queue() | 
|  | 115 | +            end | 
| 72 | 116 |         else | 
| 73 |  | -            append!(QUEUE, ptrs) | 
|  | 117 | +            Base.@lock QUEUE.lock append!(QUEUE.items, ptrs) | 
| 74 | 118 |         end | 
| 75 | 119 |     end | 
| 76 |  | -    return | 
|  | 120 | +    nothing | 
|  | 121 | +end | 
|  | 122 | + | 
|  | 123 | +""" | 
|  | 124 | +    GCHook() | 
|  | 125 | +
 | 
|  | 126 | +An immortal object which frees any pending Python objects when Julia's GC runs. | 
|  | 127 | +
 | 
|  | 128 | +This works by creating it but not holding any strong reference to it, so it is eligible | 
|  | 129 | +to be finalized by Julia's GC. The finalizer empties the PythonCall GC queue if | 
|  | 130 | +possible. The finalizer also re-attaches itself, so the object does not actually get | 
|  | 131 | +collected and so the finalizer will run again at next GC. | 
|  | 132 | +""" | 
|  | 133 | +mutable struct GCHook | 
|  | 134 | +    function GCHook() | 
|  | 135 | +        finalizer(_gchook_finalizer, new()) | 
|  | 136 | +    end | 
|  | 137 | +end | 
|  | 138 | + | 
|  | 139 | +function _gchook_finalizer(x) | 
|  | 140 | +    if C.CTX.is_initialized | 
|  | 141 | +        finalizer(_gchook_finalizer, x) | 
|  | 142 | +        if !isempty(QUEUE.items) && C.PyGILState_Check() == 1 | 
|  | 143 | +            unsafe_free_queue() | 
|  | 144 | +        end | 
|  | 145 | +    end | 
|  | 146 | +    nothing | 
|  | 147 | +end | 
|  | 148 | + | 
|  | 149 | +function __init__() | 
|  | 150 | +    HOOK[] = WeakRef(GCHook()) | 
|  | 151 | +    nothing | 
| 77 | 152 | end | 
| 78 | 153 | 
 | 
| 79 | 154 | end # module GC | 
0 commit comments