-
Notifications
You must be signed in to change notification settings - Fork 66
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
make python GC always run on thread 1 #520
Closed
Closed
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
5472d8b
make python GC always run on thread 1
ericphanson 4cb1b46
unnecessary
ericphanson d34ef10
update docstrings
ericphanson 433d928
use `Base.@lock` for nicer syntax
ericphanson 364a6f8
use `errormonitor` conditionally
ericphanson 1325e75
up
ericphanson e3a28fb
add comment
ericphanson 0271a9c
add some instrumentation
ericphanson 49f736e
improve single-thread performance
ericphanson 2584d15
don't forget the GIL
ericphanson 854e8e9
test multithreaded in CI
ericphanson dbdad78
add comments, test multithreaded
ericphanson 6f42fc5
cleanup GC code (add comments, delete old code)
ericphanson d84bb1b
turn on signal handling for multithreaded julia
ericphanson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
Manifest.toml | ||
Manifest-*.toml | ||
.ipynb_checkpoints | ||
__pycache__ | ||
*.egg-info | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
using PythonCall, Test | ||
|
||
function wait_for_queue_to_be_empty() | ||
ret = timedwait(5) do | ||
isempty(PythonCall.GC.QUEUE) | ||
end | ||
ret === :ok || error("QUEUE still not empty") | ||
end | ||
|
||
# https://github.com/JuliaCI/GCBenchmarks/blob/fc288c696381ebfdef8f002168addd0ec1b08e34/benches/serial/append/append.jl | ||
macro gctime(ex) | ||
quote | ||
local prior = PythonCall.GC.SECONDS_SPENT_IN_GC[] | ||
local ret = @timed $(esc(ex)) | ||
Base.time_print(stdout, ret.time * 1e9, ret.gcstats.allocd, ret.gcstats.total_time, Base.gc_alloc_count(ret.gcstats); msg="Runtime") | ||
println(stdout) | ||
local waiting = @elapsed wait_for_queue_to_be_empty() | ||
local after = PythonCall.GC.SECONDS_SPENT_IN_GC[] | ||
Base.time_print(stdout, (after - prior) * 1e9; msg="Python GC time") | ||
println(stdout) | ||
Base.time_print(stdout, waiting * 1e9; msg="Python GC time (waiting)") | ||
println(stdout) | ||
ret.value | ||
end | ||
end | ||
|
||
function append_lots(iters=100 * 1024, size=1596) | ||
v = pylist() | ||
for i = 1:iters | ||
v.append(pylist(rand(size))) | ||
end | ||
return v | ||
end | ||
|
||
GC.enable_logging(false) | ||
PythonCall.GC.enable_logging(false) | ||
@time "Total" begin | ||
@gctime append_lots() | ||
@time "Next full GC" begin | ||
GC.gc(true) | ||
wait_for_queue_to_be_empty() | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,77 +9,196 @@ module GC | |
|
||
using ..C: C | ||
|
||
const ENABLED = Ref(true) | ||
const QUEUE = C.PyPtr[] | ||
# `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() | ||
|
||
# This is used for basic profiling | ||
const SECONDS_SPENT_IN_GC = Threads.Atomic{Float64}() | ||
|
||
const LOGGING_ENABLED = Ref{Bool}(false) | ||
|
||
""" | ||
PythonCall.GC.disable() | ||
PythonCall.GC.enable_logging(enable=true) | ||
|
||
Enables printed logging (similar to Julia's `GC.enable_logging`). | ||
""" | ||
function enable_logging(enable=true) | ||
LOGGING_ENABLED[] = enable | ||
return nothing | ||
end | ||
|
||
Disable the PythonCall garbage collector. | ||
""" | ||
PythonCall.GC.disable() | ||
|
||
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. | ||
Disable the PythonCall garbage collector. This should generally not be required. | ||
|
||
Like most PythonCall functions, you must only call this from the main thread. | ||
""" | ||
function disable() | ||
ENABLED[] = false | ||
reset(ENABLED_EVENT) | ||
return | ||
end | ||
|
||
""" | ||
PythonCall.GC.enable() | ||
|
||
Re-enable the PythonCall garbage collector. | ||
Re-enable the PythonCall garbage collector. This should generally not be required. | ||
|
||
This frees any Python objects which were finalized while the GC was disabled, and allows | ||
objects finalized in the future to be freed immediately. | ||
|
||
Like most PythonCall functions, you must only call this from the main thread. | ||
""" | ||
function enable() | ||
ENABLED[] = true | ||
if !isempty(QUEUE) | ||
C.with_gil(false) do | ||
for ptr in QUEUE | ||
if ptr != C.PyNULL | ||
C.Py_DecRef(ptr) | ||
notify(ENABLED_EVENT) | ||
return | ||
end | ||
|
||
# This is called within a finalizer so we must not task switch | ||
# (so no printing nor blocking on Julia-side locks) | ||
function enqueue_wrapper(f, g) | ||
t = @elapsed begin | ||
if C.CTX.is_initialized | ||
# Eager path: if we are already on thread 1, | ||
# we eagerly decrement | ||
handled = false | ||
if ENABLED[] && Threads.threadid() == 1 | ||
# temporarily disable thread migration to be sure | ||
# we call `C.Py_DecRef` from thread 1 | ||
old_sticky = current_task().sticky | ||
if !old_sticky | ||
current_task().sticky = true | ||
end | ||
if Threads.threadid() == 1 | ||
f() | ||
handled = true | ||
end | ||
if !old_sticky | ||
current_task().sticky = old_sticky | ||
end | ||
end | ||
if !handled | ||
g() | ||
end | ||
end | ||
end | ||
empty!(QUEUE) | ||
Threads.atomic_add!(SECONDS_SPENT_IN_GC, t) | ||
return | ||
end | ||
|
||
function enqueue(ptr::C.PyPtr) | ||
if ptr != C.PyNULL && C.CTX.is_initialized | ||
if ENABLED[] | ||
C.with_gil(false) do | ||
# if we are on thread 1: | ||
f = () -> begin | ||
C.with_gil(false) do | ||
if ptr != C.PyNULL | ||
C.Py_DecRef(ptr) | ||
end | ||
else | ||
push!(QUEUE, ptr) | ||
end | ||
end | ||
return | ||
# otherwise: | ||
g = () -> begin | ||
if ptr != C.PyNULL | ||
put!(QUEUE, ptr) | ||
end | ||
end | ||
enqueue_wrapper(f, g) | ||
end | ||
|
||
function enqueue_all(ptrs) | ||
if C.CTX.is_initialized | ||
if ENABLED[] | ||
C.with_gil(false) do | ||
for ptr in ptrs | ||
if ptr != C.PyNULL | ||
C.Py_DecRef(ptr) | ||
end | ||
# if we are on thread 1: | ||
f = () -> begin | ||
C.with_gil(false) do | ||
for ptr in ptrs | ||
if ptr != C.PyNULL | ||
C.Py_DecRef(ptr) | ||
end | ||
end | ||
else | ||
append!(QUEUE, ptrs) | ||
end | ||
end | ||
return | ||
# otherwise: | ||
g = () -> begin | ||
for ptr in ptrs | ||
if ptr != C.PyNULL | ||
put!(QUEUE, ptr) | ||
end | ||
end | ||
end | ||
enqueue_wrapper(f, g) | ||
end | ||
|
||
# must only be called from thread 1 by the task in `GC_TASK[]` | ||
function unsafe_process_queue!() | ||
n = 0 | ||
if !isempty(QUEUE) | ||
t = @elapsed 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) | ||
n += 1 | ||
end | ||
end | ||
end | ||
if LOGGING_ENABLED[] | ||
Base.time_print(stdout, t; msg="Python GC ($n items)") | ||
println(stdout) | ||
end | ||
else | ||
t = 0.0 | ||
end | ||
return t | ||
end | ||
|
||
function gc_loop() | ||
while true | ||
if ENABLED[] && !isempty(QUEUE) | ||
t = unsafe_process_queue!() | ||
Threads.atomic_add!(SECONDS_SPENT_IN_GC, t) | ||
# 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) | ||
Comment on lines
+186
to
+190
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could wait for the GC_finished event here to preserve the semantics that this function call only returns after all GC events are processed