GC problems with `jl_gc_unsafe_enter` with multithreaded embedding

I am currently developping a Kotlin library to embed Julia in the JVM, with the goal to support multithreading with jl_adopt_thread(). The main issue is that when using some frameworks, there may not be a way to be in control of which JVM thread would want to use Julia.

If Julia is used through JVM worker threads, then you would want to make sure that after the work is done, the adopted JVM thread does not prevent the GC from running: good management of the thread’s gc_state is required.

What I came up with is the following code (simplified):

fun runUsingJulia(func: () -> Unit) {
    var wasThreadAdopted = false
    if (!isThreadAdopted()) {
        jl_adopt_thread()
        wasThreadAdopted = true
    }

    val oldState = jl_gc_unsafe_enter()
    try {
        func()
    } finally {
        jl_gc_unsafe_leave(oldState)
        if (wasThreadAdopted) {
            jl_gc_safe_enter()
        }
    }
}

Then runUsingJulia(someFunction) would:

  • Adopt the current thread if needed
  • Mark the current thread as running Julia code (GC unsafe)
  • Run someFunction
  • Mark the current thread as GC safe (jl_gc_safe_enter is needed as jl_adopt_thread marks the thread as GC unsafe by default, and therefore must be called once)

However, this is not robust, as jl_gc_unsafe_enter will segfault if the GC is running.
The first lines of jl_adopt_thread show how to deal with a similar edge-case, but:

  • jl_gc_running is not exported: it is not possible to know if it is safe to call jl_gc_unsafe_enter beforehand
  • jl_gc_disable_counter is not exported: there is no way to prevent the GC from starting before we call jl_gc_unsafe_enter (writes should also be atomic, which is not possible within the JVM)
  • jl_safepoint_wait_gc would be a good way to wait for the GC to finish (plus dealing with another edge-case where the thread is marked as GC unsafe and the GC is currently waiting for it), but it is not exported

Therefore I cannot use those from Kotlin to prevent jl_gc_unsafe_enter to segfault.

Is there any way to adopt threads for temporary work with Julia without preventing GC and edge-cases?

I should mention that I managed to make this work reliably in Windows, where jl_gc_running and jl_safepoint_wait_gc are accessble for some reason. Obviously, supporting all platforms would be better.

1 Like

Hi Keluaa,

Thanks for the interesting post. I’ve been working on a similar problem integrating C++ threads using jl_adopt_threads, and while I don’t have a solution I have the following observations from Julia 1.10:

  • jl_gc_unsafe_enter() / jl_gc_unsafe_leave() / jl_gc_safe_enter() used as above works on Windows but not on Linux as a thread ends up in a jl_gc_wait_for_the_world() which halts execution
  • jl_gc_running() and jl_safepoint_wait_gc() are not exposed on Windows any longer

On Linux I currently resolve to our old ways of using a single C++ thread to perform all calls to Julia, but on Windows I seem to be able to make use of the jl_adopt_thread() and use threading. But I’m looking out for segfaults and other strange behavior.

/Bjorn

Thank you, it reassures me that I am not the only one who stumbled on this problem, which made me give up on using Julia from Kotlin.

I think that most of the work around jl_adopt_thread was done to embed multi-threaded applications from Julia, and not the opposite, which is what we want here.

Looking at recent commits to src/threading.c, this addition could solve the issue (the commit message has a lot of info), it might be worth using a nightly build to see if it could work or not.

1 Like