Acquiring locks in finalizers

The manual has an example with a finalizer that acquires a lock (without blocking). However, in the Locks section it also says:

The following are definitely leaf locks (level 1), and must not try to acquire any other lock:

  • finalizers

So, is it actually safe for a finalizer to acquire locks?

The way this is done in the linked example (assuming you’re talking about point 2, where it talks about the Distributed stdlib) makes it ok in that very narrow context.

In general though, it’s not safe to acquire locks in a finalizer. Usually there’s room for a design that doesn’t require that too. Do you have an example where you need this?

So I’m wrapping a C library that has a Context type which owns the memory of many Expression types, where each Expression is reference counted. I want to call free and dec_ref on them via the finalizer, similar to how the python wrapper does it via __del__.

When both Context and some Expression are out of scope in Julia, the finalizers on them may be called in any order. However, calling dec_ref on Expression after calling free on Context will segfault, because freeing the context already deallocates the memory holding the expression.

To prevent the above, I need to make sure to never call dec_ref on Expression after calling free on Context, and also to make sure they are not called at the same time. For the latter I can use a lock, and the finalizers for the types will acquire the lock before freeing the memory. It’ll look like this:

mutable struct Context
    ctx::C_context
    finalized::Bool
    lock::ReentrantLock
    function Context(ctx::C_context)
        c = new(ctx, false, ReentrantLock())
        finalizer(finalize_ctx, c)
    end
end

function finalize_ctx(c)
  if islocked(c.lock) || !trylock(c.lock)
    finalizer(finalize_ctx, c)
    return nothing
  end
  try
    c.finalized = true
    C_del_context(c.ctx)
  finally
    unlock(c.lock)
  end
end

mutable struct Expr <: AST
    ctx::Context
    ast::C_expr
    function Expr(ctx::Context, ast::C_expr)
        e = new(ctx, ast)
        C_inc_ref(e.ctx, e.ast)
        finalizer(finalize_expr, e)
    end
end

function finalize_expr(e)
  if !e.ctx.finalized # no need to free e if ctx is already freed
    if islocked(e.ctx.lock) || !trylock(e.ctx.lock)
      finalizer(finalize_expr, e)
      return nothing
    end
    try
      C_dec_ref(e.ctx, e.ast)
    finally
      unlock(e.ctx.lock)
    end
  end
end

Would this be OK?

In FFTW.jl, because destroying plans is not thread-safe, we jump through some hoops in the finalizer, which calls a function maybe_destroy_plan: FFTW.jl/src/fft.jl at f888022d7a1ff78491abf8f33f1055cc52a68f0a · JuliaMath/FFTW.jl · GitHub

My understanding is that you can use a trylock spinloop as long as you call GC.safepoint() in the loop according to @jameson?