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