Are finalizers guaranteed to only run at most one time? Is there any way for a finalizer to somehow get called twice, for example, if finalize is called in multiple other finalizers, or manually called in different threads?
I am trying to manually free memory not owned by Julia when I know it won’t be used later, but also have a finalizer as a fallback to avoid memory leaks.
In my experiments I have never seen finalize running more than once per registration and I guess that it’s mutli-threading safe, but I haven’t checked the code. That being said, you probably (I haven’t tested that) can run it multiple times if you register it multiple times. Note, that you can register during the finalize call so you can decide at the latest possible point if you want to have it run again in the future (however that could possibly lead to a problematic looping behavior).
However, not using finalize can still be beneficial due to being deterministic and low-latency (probably also leading to a better memory locality usage pattern). You can use a do block to have it called semi-automatically (automatically from the caller’s perspective). If you want to mostly ensure that the memory gets freed, you can use some parts of a try-catch-finally-else block and probably hide that behind a function, too.
If you like the ease of finalize to contain all the relevant state needed for freeing the memory, you can create an anonymous function containing that state and save it as a field in your struct and call that with the mentioned mechanisms when you know that you do not need it anymore.
So I think the answer is no: Calling finalize manually can lead to use-after-free but not to double-free.
The protecting lock is global. So you may have contention if you call lots of finalizers manually from many threads. But the critical section doesn’t include the actual finalization code.
On the other hand, if I understand the code right, you can install many finalizers on many threads without contention (i.e. multi-threaded BigInt code doesn’t suck).
If the pointer is stored in a mutable object, I would set the pointer to C_NULL immediately after freeing it to prevent it from being freed again. The finalizer should check if the pointer is null before trying tk free.