I don't understand why there's a need for another level of indirection when releasing or acquiring the GVL in Ruby C API.
Both rb_thread_call_without_gvl()
and rb_thread_call_with_gvl()
require a function that accepts only one argument which isn't always the case.
I don't want to wrap my arguments in a struct just for the purpose of releasing the GVL. It complicates the code's readability and requires casting from and to void pointers.
After looking into Ruby's threading code I found the GVL_UNLOCK_BEGIN
/GVL_UNLOCK_END
macros that matches Python's Py_BEGIN_ALLOW_THREADS
/Py_END_ALLOW_THREADS
but I can't find documentation about them and when they are safe to use.
There's also the BLOCKING_REGION
macro is used within rb_thread_call_without_gvl()
but I'm not sure if it's safe to use it as a standalone without calling rb_thread_call_without_gvl()
itself.
What is the correct way to safely release the GVL in the middle of the execution flow without having to call another function?
In Ruby 2.x, there is only the rb_thread_call_without_gvl
API. GVL_UNLOCK_BEGIN
and GVL_UNLOCK_END
are implementation details that are only defined in thread.c
, and are therefore unavailable to Ruby extensions. Thus, the direct answer to your question is "there is no way to correctly and safely release the GVL without calling another function".
There was previously a "region-based" API, rb_thread_blocking_region_begin
/rb_thread_blocking_region_end
, but this API was deprecated in Ruby 1.9.3 and removed in Ruby 2.2 (see https://bugs.ruby-lang.org/projects/ruby-trunk/wiki/CAPI_obsolete_definitions for the CAPI deprecation schedule).
Therefore, unfortunately, you are stuck with rb_thread_call_without_gvl
.
That said, there's a few things you could do to ease the pain. In standard C, converting between most pointers and void *
is implicit, so you don't have to add a cast. Furthermore, using designated initializer syntax can simplify the creation of the argument structure.
Thus, you can write
struct my_func_args {
int arg1;
char *arg2;
};
void *func_no_gvl(void *data) {
struct my_func_args *args = data;
/* do stuff with args->arg... */
return NULL;
}
VALUE my_ruby_function(...) {
...
struct my_func_args args = {
// designated initializer syntax (C99) for cleaner code
.arg1 = ...,
.arg2 = ...,
};
// call without an unblock function
void *res = rb_thread_call_without_gvl(func_no_gvl, &args, NULL, NULL);
...
}
Although this doesn't solve your original problem, it does at least make it more tolerable (I hope).
What is the correct way to safely release the GVL in the middle of the execution flow without having to call another function?
You must use the supplied API or whatever method you use will eventually break. The API to the GVL is defined in thread.h
void *rb_thread_call_with_gvl(void *(*func)(void *), void *data1);
void *rb_thread_call_without_gvl(void *(*func)(void *), void *data1,
rb_unblock_function_t *ubf, void *data2);
void *rb_thread_call_without_gvl2(void *(*func)(void *), void *data1,
rb_unblock_function_t *ubf, void *data2);
What you find in the header is an agreement between you the consumer of their API's and the author of the API's. Think of it as a contract. Anything you find in a .c
in particular static methods and MACROS are not for consumption outside the file unless it's found in the header. The static
keyword prevents this from happening, it's one of the reason it exists and it's most important use in C. The other items you mentioned are in thread.c
. You can poke around in thread.c
but using anything from it is a violation of the API's contract ie it's not safe and never will be.
I'm not suggesting you do this but the only way for you to do what you want is to copy portions of their implementation into your own code and this would not pass a code review. The amount of code you would need to copy out would likely dwarf anything you would need to do to use their API's safely.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With