Skip to content

Commit 597cbc8

Browse files
committed
Run C extensions marked as rb_ext_ractor_safe()/rb_ext_thread_safe() without the C extension lock
* Fixes #2136. * Rename *c_mutex* methods to *cext_lock* for consistency and clarity.
1 parent fbc70ba commit 597cbc8

File tree

15 files changed

+188
-36
lines changed

15 files changed

+188
-36
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Compatibility:
1818

1919
Performance:
2020

21+
* Run C extensions marked as `rb_ext_ractor_safe()` or `rb_ext_thread_safe()` in parallel (without the C extension lock) (#2136, @eregon).
2122

2223
Changes:
2324

doc/user/benchmarking.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ See [this documentation](reporting-performance-problems.md) for more details abo
4444

4545
### Consider Disabling the Global C-Extension Lock
4646

47-
On TruffleRuby, C extensions by default use a global lock for maximum compatibility with CRuby.
48-
If you are benchmarking a multi-threaded Ruby program (e.g. Rails on a multi-threaded server), it is worth trying
47+
On TruffleRuby, C extensions by default use a global extension lock for maximum compatibility with CRuby.
48+
Extensions marked as thread-safe by using `rb_ext_ractor_safe()` or `rb_ext_thread_safe()` do not use the global extension lock and run in parallel.
49+
50+
If you are benchmarking a multi-threaded Ruby program (e.g. Rails on a multi-threaded server),
51+
and not all extensions are already marked as thread-safe, it is worth trying
4952
`TRUFFLERUBYOPT="--experimental-options --cexts-lock=false"`.
50-
[This issue](https://github.com/oracle/truffleruby/issues/2136) tracks a way to automatically not use the lock for extensions which do not need it.
5153

5254
## Recommendations
5355

doc/user/compatibility.md

+9
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ To help alleviate this problem, backtraces are automatically disabled in cases w
194194

195195
## C Extension Compatibility
196196

197+
### Global Extension Lock
198+
199+
Native extensions are by default considered thread-unsafe for maximum compatibility with CRuby and use the global extension lock (unless `--cexts-lock=false` is used).
200+
201+
Extensions can mark themselves as thread-safe either by using `rb_ext_ractor_safe()` or `rb_ext_thread_safe()`.
202+
Such extensions are then run by TruffleRuby without a global extension lock, i.e. in parallel.
203+
204+
See [Thread-Safe Extensions](thread-safe-extensions.md) for how to mark extensions as Ractor-safe or thread-safe.
205+
197206
### Identifiers may be macros or functions
198207

199208
Identifiers which are normally macros may be functions, functions may be macros, and global variables may be macros.

doc/user/thread-safe-extensions.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
layout: docs-experimental
3+
toc_group: ruby
4+
link_title: Thread-Safe Extensions
5+
permalink: /reference-manual/ruby/ThreadSafeExtensions/
6+
---
7+
# Thread-Safe Extensions
8+
9+
Native extensions are by default considered thread-unsafe for maximum compatibility with CRuby and use the global extension lock (unless `--cexts-lock=false` is used).
10+
11+
Extensions can mark themselves as thread-safe either by using `rb_ext_ractor_safe()` or `rb_ext_thread_safe()` (TruffleRuby-specific).
12+
Such extensions are then run by TruffleRuby without a global extension lock, i.e. in parallel.
13+
14+
Here is an example of an extension marking itself as Ractor-safe.
15+
Such an extension must then satisfy [the conditions for Ractor-safe extensions](https://github.com/ruby/ruby/blob/master/doc/extension.rdoc#appendix-f-ractor-support-).
16+
```c
17+
void Init_my_extension(void) {
18+
#ifdef HAVE_RB_EXT_RACTOR_SAFE
19+
rb_ext_ractor_safe(true);
20+
#endif
21+
22+
rb_define_method(myClass, "foo", foo_impl, 0); // The C function foo_impl can be called from multiple threads in parallel
23+
}
24+
```
25+
26+
Here is an example of an extension marking itself as thread-safe:
27+
```c
28+
void Init_my_extension(void) {
29+
#ifdef HAVE_RB_EXT_THREAD_SAFE
30+
rb_ext_thread_safe(true);
31+
#endif
32+
33+
rb_define_method(myClass, "foo", foo_impl, 0); // The C function foo_impl can be called from multiple threads in parallel
34+
}
35+
```
36+
37+
The conditions for an extension to be thread-safe are the following.
38+
This is similar to [the conditions for Ractor-safe extensions](https://github.com/ruby/ruby/blob/master/doc/extension.rdoc#appendix-f-ractor-support-) but not all conditions are necessary.
39+
1. The extension should make it clear in its documentation which objects are safe to share between threads and which are not.
40+
This already needs to be done on CRuby with the GVL, as threads run concurrently.
41+
It helps gem users to avoid sharing objects between threads incorrectly.
42+
2. The extension's own code must be thread-safe, e.g. not mutate state shared between threads without synchronization.
43+
For example accesses to a `struct` shared between threads should typically be synchronized if it's not immutable.
44+
3. If the extension calls native library functions which are not thread-safe it must ensure that function cannot be called from multiple threads at the same time, e.g. using a [lock](https://github.com/oracle/truffleruby/blob/fd8dc74a72d107f8e58feaf1be1cfbb2f31d2e85/lib/cext/include/ruby/thread_native.h).
45+
4. Ruby C API functions/macros (like `rb_*()`) are generally thread-safe on TruffleRuby, because most of them end up calling some Ruby method.
46+
47+
These are the differences in comparison to Ractor-safe:
48+
* It is allowed to share Ruby objects between multiple threads from an extension, because it is the same as sharing them with only Ruby code.
49+
* There is no need to mark objects as Ractor-shareable.
50+
51+
Another way to look at this is to reason about the guarantees that a global extension lock provides:
52+
* C functions or sections of C code which does __*not*__ use any Ruby C API functions/macros (like `rb_*()`) are executed sequentially, i.e. one after another.
53+
* Calls to any Ruby C API function/macro have the possibility to trigger thread switching, and so for another part of the extension code to execute, while the current function is "suspended".
54+
* Therefore functions given to `rb_define_method`, if they call Ruby C API functions/macros (very likely), do not really benefit from the global extension lock as thread switching can happen in the middle of them, and they already need to take care about other functions executing in between.

lib/cext/include/ruby/internal/intern/load.h

+5
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ void rb_ext_ractor_safe(bool flag);
248248
*/
249249
#define HAVE_RB_EXT_RACTOR_SAFE 1
250250

251+
#ifdef TRUFFLERUBY
252+
void rb_ext_thread_safe(bool flag);
253+
#define RB_EXT_THREAD_SAFE(f) rb_ext_thread_safe(f)
254+
#define HAVE_RB_EXT_THREAD_SAFE 1
255+
#endif
251256
/** @} */
252257

253258
RBIMPL_SYMBOL_EXPORT_END()

lib/cext/include/truffleruby/truffleruby-abi-version.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
// $RUBY_VERSION must be the same as TruffleRuby.LANGUAGE_VERSION.
2121
// $ABI_NUMBER starts at 1 and is incremented for every ABI-incompatible change.
2222

23-
#define TRUFFLERUBY_ABI_VERSION "3.3.7.1"
23+
#define TRUFFLERUBY_ABI_VERSION "3.3.7.2"
2424

2525
#endif

lib/truffle/truffle/cext.rb

+21-17
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def init_extension(library, library_path)
236236

237237
init_function = library[function_name]
238238

239-
Primitive.call_with_c_mutex_and_frame(-> {
239+
Primitive.call_with_cext_lock_and_frame(-> {
240240
begin
241241
Primitive.interop_execute(VOID_TO_VOID_WRAPPER, [init_function])
242242
ensure
@@ -260,6 +260,10 @@ def check_abi_version(embedded_abi_version, extension_path)
260260
end
261261
end
262262

263+
def rb_tr_cext_lock_owned_p
264+
Primitive.cext_lock_owned?
265+
end
266+
263267
def rb_stdin
264268
$stdin
265269
end
@@ -853,7 +857,7 @@ def rb_str_new_frozen(value)
853857

854858
def rb_tracepoint_new(events, func, data)
855859
TracePoint.new(*events_to_events_array(events)) do |tp|
856-
Primitive.call_with_c_mutex_and_frame(
860+
Primitive.call_with_cext_lock_and_frame(
857861
POINTER2_TO_VOID_WRAPPER,
858862
[func, Primitive.cext_wrap(tp), data],
859863
Primitive.caller_special_variables_if_available,
@@ -1201,7 +1205,7 @@ def rb_path_to_class(path)
12011205

12021206
def rb_proc_new(function, value)
12031207
Proc.new do |*args, &block|
1204-
Primitive.call_with_c_mutex_and_frame_and_unwrap(RB_BLOCK_CALL_FUNC_WRAPPER, [
1208+
Primitive.call_with_cext_lock_and_frame_and_unwrap(RB_BLOCK_CALL_FUNC_WRAPPER, [
12051209
function,
12061210
Primitive.cext_wrap(args.first), # yieldarg
12071211
Primitive.cext_wrap(value), # procarg,
@@ -1472,7 +1476,7 @@ def rb_enumeratorize(obj, meth, args)
14721476
def rb_enumeratorize_with_size(obj, meth, args, size_fn)
14731477
return rb_enumeratorize(obj, meth, args) if Primitive.interop_null?(size_fn)
14741478
enum = obj.to_enum(meth, *args) do
1475-
Primitive.call_with_c_mutex_and_frame_and_unwrap(
1479+
Primitive.call_with_cext_lock_and_frame_and_unwrap(
14761480
POINTER3_TO_POINTER_WRAPPER,
14771481
[size_fn, Primitive.cext_wrap(obj), Primitive.cext_wrap(args), Primitive.cext_wrap(enum)],
14781482
Primitive.caller_special_variables_if_available,
@@ -1491,7 +1495,7 @@ def rb_newobj_of(ruby_class)
14911495

14921496
def rb_define_alloc_func(ruby_class, function)
14931497
ruby_class.singleton_class.define_method(:__allocate__) do
1494-
Primitive.call_with_c_mutex_and_frame_and_unwrap(
1498+
Primitive.call_with_cext_lock_and_frame_and_unwrap(
14951499
POINTER_TO_POINTER_WRAPPER,
14961500
[function, Primitive.cext_wrap(self)],
14971501
Primitive.caller_special_variables_if_available,
@@ -1694,7 +1698,7 @@ def rb_nativethread_lock_destroy(lock)
16941698

16951699
def rb_set_end_proc(func, data)
16961700
at_exit do
1697-
Primitive.call_with_c_mutex_and_frame(
1701+
Primitive.call_with_cext_lock_and_frame(
16981702
POINTER_TO_VOID_WRAPPER, [func, data],
16991703
Primitive.caller_special_variables_if_available, nil)
17001704
end
@@ -1733,7 +1737,7 @@ def RTYPEDDATA(object)
17331737
private def data_sizer(sizer_function, rtypeddata)
17341738
raise unless sizer_function.respond_to?(:call)
17351739
proc {
1736-
Primitive.call_with_c_mutex_and_frame(
1740+
Primitive.call_with_cext_lock_and_frame(
17371741
POINTER_TO_SIZE_T_WRAPPER, [sizer_function, rtypeddata],
17381742
Primitive.caller_special_variables_if_available, nil)
17391743
}
@@ -1775,7 +1779,7 @@ def rb_data_typed_object_wrap(ruby_class, data, data_type, mark, free, size)
17751779
end
17761780

17771781
def run_data_finalizer(function, data)
1778-
Primitive.call_with_c_mutex_and_frame POINTER_TO_VOID_WRAPPER, [function, data], nil, nil
1782+
Primitive.call_with_cext_lock_and_frame POINTER_TO_VOID_WRAPPER, [function, data], nil, nil
17791783
end
17801784

17811785
def run_marker(obj)
@@ -1832,7 +1836,7 @@ def send_splatted(object, method, args)
18321836

18331837
def rb_block_call(object, method, args, func, data)
18341838
object.__send__(method, *args) do |*block_args|
1835-
Primitive.cext_unwrap(Primitive.call_with_c_mutex(RB_BLOCK_CALL_FUNC_WRAPPER, [ # Probably need to save the frame here for blocks.
1839+
Primitive.cext_unwrap(Primitive.call_with_cext_lock(RB_BLOCK_CALL_FUNC_WRAPPER, [ # Probably need to save the frame here for blocks.
18361840
func,
18371841
Primitive.cext_wrap(block_args.first),
18381842
data,
@@ -1917,7 +1921,7 @@ def rb_exec_recursive(func, obj, arg)
19171921

19181922
def rb_catch_obj(tag, func, data)
19191923
catch tag do |caught|
1920-
Primitive.cext_unwrap(Primitive.call_with_c_mutex(RB_BLOCK_CALL_FUNC_WRAPPER, [
1924+
Primitive.cext_unwrap(Primitive.call_with_cext_lock(RB_BLOCK_CALL_FUNC_WRAPPER, [
19211925
func,
19221926
Primitive.cext_wrap(caught),
19231927
Primitive.cext_wrap(data),
@@ -2003,12 +2007,12 @@ def rb_time_interval_acceptable(time_val)
20032007

20042008
def rb_thread_create(fn, args)
20052009
Thread.new do
2006-
Primitive.call_with_c_mutex_and_frame(POINTER_TO_POINTER_WRAPPER, [fn, args], Primitive.caller_special_variables_if_available, nil)
2010+
Primitive.call_with_cext_lock_and_frame(POINTER_TO_POINTER_WRAPPER, [fn, args], Primitive.caller_special_variables_if_available, nil)
20072011
end
20082012
end
20092013

20102014
def rb_thread_call_with_gvl(function, data)
2011-
Primitive.call_with_c_mutex(POINTER_TO_POINTER_WRAPPER, [function, data])
2015+
Primitive.call_with_cext_lock(POINTER_TO_POINTER_WRAPPER, [function, data])
20122016
end
20132017

20142018
def rb_thread_call_without_gvl(function, data1, unblock, data2)
@@ -2033,7 +2037,7 @@ def rb_thread_call_without_gvl(function, data1, unblock, data2)
20332037
def rb_iterate(iteration, iterated_object, callback, callback_arg)
20342038
block = rb_block_proc
20352039
wrapped_callback = proc do |block_arg|
2036-
Primitive.call_with_c_mutex_and_frame_and_unwrap(RB_BLOCK_CALL_FUNC_WRAPPER, [
2040+
Primitive.call_with_cext_lock_and_frame_and_unwrap(RB_BLOCK_CALL_FUNC_WRAPPER, [
20372041
callback,
20382042
Primitive.cext_wrap(block_arg),
20392043
Primitive.cext_wrap(callback_arg),
@@ -2043,7 +2047,7 @@ def rb_iterate(iteration, iterated_object, callback, callback_arg)
20432047
], Primitive.cext_special_variables_from_stack, block)
20442048
end
20452049
Primitive.cext_unwrap(
2046-
Primitive.call_with_c_mutex_and_frame(POINTER_TO_POINTER_WRAPPER, [
2050+
Primitive.call_with_cext_lock_and_frame(POINTER_TO_POINTER_WRAPPER, [
20472051
iteration,
20482052
Primitive.cext_wrap(iterated_object)
20492053
], Primitive.cext_special_variables_from_stack, wrapped_callback))
@@ -2145,15 +2149,15 @@ def rb_define_hooked_variable(name, gvar, getter, setter)
21452149
id = name.to_sym
21462150

21472151
getter_proc = -> {
2148-
Primitive.call_with_c_mutex_and_frame_and_unwrap(
2152+
Primitive.call_with_cext_lock_and_frame_and_unwrap(
21492153
POINTER2_TO_POINTER_WRAPPER,
21502154
[getter, Primitive.cext_wrap(id), gvar],
21512155
Primitive.caller_special_variables_if_available,
21522156
nil)
21532157
}
21542158

21552159
setter_proc = -> value {
2156-
Primitive.call_with_c_mutex_and_frame(
2160+
Primitive.call_with_cext_lock_and_frame(
21572161
POINTER3_TO_VOID_WRAPPER,
21582162
[setter, Primitive.cext_wrap(value), Primitive.cext_wrap(id), gvar],
21592163
Primitive.caller_special_variables_if_available,
@@ -2282,7 +2286,7 @@ def rb_fiber_current
22822286

22832287
def rb_fiber_new(function, value)
22842288
Fiber.new do |*args|
2285-
Primitive.call_with_c_mutex_and_frame_and_unwrap(RB_BLOCK_CALL_FUNC_WRAPPER, [
2289+
Primitive.call_with_cext_lock_and_frame_and_unwrap(RB_BLOCK_CALL_FUNC_WRAPPER, [
22862290
function,
22872291
Primitive.cext_wrap(args.first), # yieldarg
22882292
nil, # procarg,

lib/truffle/truffle/cext_ruby.rb

+6-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def rb_define_method(mod, name, function, argc)
2121
end
2222

2323
wrapper = RB_DEFINE_METHOD_WRAPPERS[argc]
24+
thread_safe = Primitive.cext_thread_safe?
2425
method_body = Truffle::Graal.copy_captured_locals -> *args, &block do
2526
if argc == -1 # (int argc, VALUE *argv, VALUE obj)
2627
args = [function, args.size, Truffle::CExt.RARRAY_PTR(args), Primitive.cext_wrap(self)]
@@ -35,7 +36,11 @@ def rb_define_method(mod, name, function, argc)
3536

3637
# We must set block argument if given here so that the
3738
# `rb_block_*` functions will be able to find it by walking the stack.
38-
Primitive.call_with_c_mutex_and_frame_and_unwrap(wrapper, args, Primitive.caller_special_variables_if_available, block)
39+
if thread_safe
40+
Primitive.call_with_frame_and_unwrap(wrapper, args, Primitive.caller_special_variables_if_available, block)
41+
else
42+
Primitive.call_with_cext_lock_and_frame_and_unwrap(wrapper, args, Primitive.caller_special_variables_if_available, block)
43+
end
3944
end
4045

4146
# Even if the argc is -2, the arity number

spec/truffle/capi/cext_lock_spec.rb

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# truffleruby_primitives: true
2+
13
# Copyright (c) 2021, 2025 Oracle and/or its affiliates. All rights reserved. This
24
# code is released under a tri EPL/GPL/LGPL license. You can use it,
35
# redistribute it and/or modify it under the terms of the:
@@ -16,7 +18,7 @@
1618
end
1719

1820
it "is not acquired in Ruby code" do
19-
Truffle::CExt.cext_lock_owned?.should == false
21+
Primitive.cext_lock_owned?.should == false
2022
end
2123

2224
guard -> { Truffle::Boot.get_option 'cexts-lock' } do
@@ -32,4 +34,9 @@
3234
it "is released inside rb_funcall" do
3335
@t.has_lock_in_rb_funcall?.should == false
3436
end
37+
38+
it "is not acquired for methods defined in rb_ext_ractor_safe(true) extensions" do
39+
@t.has_lock_for_rb_define_method_after_rb_ext_ractor_safe?.should == false
40+
@t.has_lock_for_rb_define_method_after_rb_ext_ractor_safe_false?.should == true
41+
end
3542
end

spec/truffle/capi/ext/truffleruby_lock_spec.c

+14-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ static VALUE has_lock_in_call_without_gvl(VALUE self) {
3030
}
3131

3232
static VALUE has_lock_in_rb_funcall(VALUE self) {
33-
return rb_funcall(truffleCExt, rb_intern("cext_lock_owned?"), 0);
33+
return rb_funcall(truffleCExt, rb_intern("rb_tr_cext_lock_owned_p"), 0);
34+
}
35+
36+
static VALUE has_lock_for_rb_define_method_after_rb_ext_ractor_safe(VALUE self) {
37+
return rb_tr_cext_lock_owned_p();
38+
}
39+
40+
static VALUE has_lock_for_rb_define_method_after_rb_ext_ractor_safe_false(VALUE self) {
41+
return rb_tr_cext_lock_owned_p();
3442
}
3543

3644
void Init_truffleruby_lock_spec(void) {
@@ -39,6 +47,11 @@ void Init_truffleruby_lock_spec(void) {
3947
rb_define_method(cls, "has_lock?", has_lock, 0);
4048
rb_define_method(cls, "has_lock_in_call_without_gvl?", has_lock_in_call_without_gvl, 0);
4149
rb_define_method(cls, "has_lock_in_rb_funcall?", has_lock_in_rb_funcall, 0);
50+
51+
rb_ext_ractor_safe(true);
52+
rb_define_method(cls, "has_lock_for_rb_define_method_after_rb_ext_ractor_safe?", has_lock_for_rb_define_method_after_rb_ext_ractor_safe, 0);
53+
rb_ext_ractor_safe(false);
54+
rb_define_method(cls, "has_lock_for_rb_define_method_after_rb_ext_ractor_safe_false?", has_lock_for_rb_define_method_after_rb_ext_ractor_safe_false, 0);
4255
}
4356

4457
#ifdef __cplusplus

src/main/c/cext/ractor.c

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212

1313
// Ractor, rb_ractor_*
1414

15-
// Because of a mix of #if HAVE_RB_EXT_RACTOR_SAFE and #ifdef HAVE_RB_EXT_RACTOR_SAFE,
16-
// we cannot just leave HAVE_RB_EXT_RACTOR_SAFE undefined or defined to 0 without getting
17-
// -Wundef warnings & errors. So we let it defined to 1 but rb_ext_ractor_safe() has no effect.
18-
// Also rb_ext_ractor_safe() is sometimes called directly instead of RB_EXT_RACTOR_SAFE().
1915
void rb_ext_ractor_safe(bool flag) {
20-
// No-op
16+
rb_ext_thread_safe(flag);
17+
}
18+
19+
void rb_ext_thread_safe(bool flag) {
20+
polyglot_invoke(RUBY_CEXT, "set_thread_safe", flag);
2121
}
2222

2323
// Simplified to main Ractor only

src/main/c/cext/truffleruby.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ VALUE rb_tr_zlib_crc_table(void) {
3939
}
4040

4141
VALUE rb_tr_cext_lock_owned_p(void) {
42-
return RUBY_CEXT_INVOKE("cext_lock_owned?");
42+
return RUBY_CEXT_INVOKE("rb_tr_cext_lock_owned_p?");
4343
}
4444

4545
// Used for internal testing

0 commit comments

Comments
 (0)