Issue #22067 has been updated by ko1 (Koichi Sasada). This was approved by Matz. ---------------------------------------- Feature #22067: New RUBY_TYPED_THREAD_SAFE_FREE bit to declare thread safe dfree functions https://bugs.ruby-lang.org/issues/22067#change-117706 * Author: luke-gru (Luke Gruber) * Status: Open ---------------------------------------- CRuby `TypedData` types are used internally in the VM and in C extensions. Currently any `dfree` functions, which usually are declared `RUBY_TYPED_FREE_IMMEDIATELY`, are run with the VM lock held and a VM barrier stopping all ractors and threads. In short, no other Ruby threads or GC threads can run while these `free` functions are called. To be specific these can be run any time an allocation (newobj or xmalloc) happens (or `CHECK_INTS`), which is when GC may trigger. The function will be called synchronously and finish before the allocation returns. Though much more relaxed than what's required without `RUBY_TYPED_FREE_IMMEDIATELY`, this expectation limits what GC implementations can do. To allow more flexibility in GC implementations, we want to let worker GC threads free these TypedData objects concurrently with other Ruby code or in parallel with other GC threads. Most `TypedData` `dfree` functions are simply calls to `xfree` and are trivially thread safe. The exceptions are `dfree` functions that read or modify global state without locking. ### Examples **Safe** ```c static void safe_data_free(void *ptr) { // thread safe safe_data *data = ptr; xfree(data->buffer); xfree(data); } ``` **Unsafe due to shared state** ```c static void example_data_free(void *ptr) { st_delete(live_example_datas, (st_data_t*)&ptr, NULL); // Not thread-safe! } ``` There are two reasons this would not be thread-safe to free: * Unsafe for parallel free - 2 of these `TypedData` objects could be freed at the same time, corrupting the `st_table` * Unsafe for concurrent free - `live_example_datas` would be inserted into and iterated by Ruby code For types like this which mutate a piece of shared state, they should either not be declared as thread-safe or some form of locking should be used when manipulating the table. Most types are trivially thread-safe, but there are enough exceptions in CRuby itself that it needs to be opt-in, the same way RUBY_TYPED_FREE_IMMEDIATELY is. ### Proposal We propose adding a new flag to `TypedData`, `RUBY_TYPED_THREAD_SAFE_FREE` that allows CRuby itself and extension authors to opt-in to declaring their `TypedData` type as thread safe. Proposed documentation to `doc/extension.rdoc` ``` RUBY_TYPED_THREAD_SAFE_FREE :: This flag declares that the dfree() function is thread-safe. With this flag set the garbage collector MAY invoke dfree() * On any thread, whether a Ruby Thread or a native thread * In parallel with calls to the same or other dfree functions * Concurrently with other Ruby code This should NOT be set on dfree() functions which access shared state in a thread-unsafe way. A dfree() function which uses only xfree() on the struct and memory it exclusively owns is thread-safe. This flag implies RUBY_TYPED_FREE_IMMEDIATELY, since a thread-safe dfree() is also safe to invoke immediately. ``` Per that doc, this is purely a more strict dfree constraint from the TypedData implementer's perspective, and more flexible from the GC author's perspective. We can treat anything with this flag set the same as `RUBY_TYPED_FREE_IMMEDIATELY` (or the same as no flag set, they are purely incremental in how "friendly" the dfree function is). Many gems can `s/RUBY_TYPED_FREE_IMMEDIATELY/RUBY_TYPED_THREAD_SAFE_FREE/g` with a cursory audit. It's helpful to have one imply the other to avoid cluttering the flags (and they happen to be the exact same length 😅). This forms a series of bits with increasingly relaxed constraints on when dfree may run: no flag (mutator thread, holding the GVL, at an interrupt check), RUBY_TYPED_FREE_IMMEDIATELY (synchronously during GC, world stopped), and RUBY_TYPED_THREAD_SAFE_FREE (any thread, in parallel, concurrent with Ruby code). ---Files-------------------------------- Screenshot_20260513_185452.webp (99.8 KB) Screenshot_20260513_192449.webp (31.9 KB) Screenshot_20260513_192734.webp (22.4 KB) Screenshot_20260513_192910.webp (27.3 KB) -- https://bugs.ruby-lang.org/