Skip to content

Commit c282d62

Browse files
authored
Merge pull request #91 from mrkn/without_gvl
Add PyCall.without_gvl
2 parents 9afdab0 + 568b603 commit c282d62

File tree

6 files changed

+204
-31
lines changed

6 files changed

+204
-31
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# The change history of PyCall
22

3+
## master
4+
5+
* Add `PyCall.without_gvl` for explicitly releasing the RubyVM GVL
6+
37
## 1.2.1
48

59
* Prevent circular require in pycall/iruby.rb

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@ the `Math.sin` in Ruby:
6161
Type conversions from Ruby to Python are automatically performed for numeric,
6262
boolean, string, arrays, and hashes.
6363

64+
### Releasing the RubyVM GVL during Python function calls
65+
66+
You may want to release the RubyVM GVL when you call a Python function that takes very long runtime.
67+
PyCall provides `PyCall.without_gvl` method for such purpose. When PyCall performs python function call,
68+
PyCall checks the current context, and then it releases the RubyVM GVL when the current context is in a `PyCall.without_gvl`'s block.
69+
70+
```ruby
71+
PyCall.without_gvl do
72+
# In this block, all Python function calls are performed without
73+
# the GVL acquisition.
74+
pyobj.long_running_function()
75+
end
76+
77+
# Outside of PyCall.without_gvl block,
78+
# all Python function calls are performed with the GVL acquisition.
79+
pyobj.long_running_function()
80+
```
81+
6482
### Debugging python finder
6583

6684
When you encounter `PyCall::PythonNotFound` error, you can investigate PyCall's python finder by setting `PYCALL_DEBUG_FIND_LIBPYTHON` environment variable to `1`. You can see the log like below:

ext/pycall/pycall.c

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,52 @@ pycall_after_fork(VALUE mod)
7070
return Qnil;
7171
}
7272

73+
static volatile pycall_tls_key without_gvl_key;
74+
75+
int
76+
pycall_without_gvl_p(void)
77+
{
78+
/*
79+
* In pthread, the default value is NULL (== 0).
80+
*
81+
* In Win32 thread, the default value is 0 (initialized by TlsAlloc).
82+
*/
83+
return (int)pycall_tls_get(without_gvl_key);
84+
}
85+
86+
static inline int
87+
pycall_set_without_gvl(void)
88+
{
89+
return pycall_tls_set(without_gvl_key, (void *)1);
90+
}
91+
92+
static inline int
93+
pycall_set_with_gvl(void)
94+
{
95+
return pycall_tls_set(without_gvl_key, (void *)0);
96+
}
97+
98+
VALUE
99+
pycall_without_gvl(VALUE (* func)(VALUE), VALUE arg)
100+
{
101+
int state;
102+
VALUE result;
103+
104+
pycall_set_without_gvl();
105+
106+
result = rb_protect(func, arg, &state);
107+
108+
pycall_set_with_gvl();
109+
110+
return result;
111+
}
112+
113+
static VALUE
114+
pycall_m_without_gvl(VALUE mod)
115+
{
116+
return pycall_without_gvl(rb_yield, Qnil);
117+
}
118+
73119
/* ==== PyCall::PyPtr ==== */
74120

75121
const rb_data_type_t pycall_pyptr_data_type = {
@@ -890,7 +936,7 @@ struct call_pyobject_call_params {
890936
PyObject *kwargs;
891937
};
892938

893-
PyObject *
939+
static inline PyObject *
894940
call_pyobject_call(struct call_pyobject_call_params *params)
895941
{
896942
PyObject *res;
@@ -899,17 +945,22 @@ call_pyobject_call(struct call_pyobject_call_params *params)
899945
}
900946

901947
PyObject *
902-
pyobject_call_without_gvl(PyObject *pycallable, PyObject *args, PyObject *kwargs)
948+
pyobject_call(PyObject *pycallable, PyObject *args, PyObject *kwargs)
903949
{
904950
PyObject *res;
905951
struct call_pyobject_call_params params;
906952
params.pycallable = pycallable;
907953
params.args = args;
908954
params.kwargs = kwargs;
909955

910-
res = (PyObject *)rb_thread_call_without_gvl(
911-
(void * (*)(void *))call_pyobject_call, (void *)&params,
912-
(rb_unblock_function_t *)pycall_interrupt_python_thread, NULL);
956+
if (pycall_without_gvl_p()) {
957+
res = (PyObject *)rb_thread_call_without_gvl(
958+
(void * (*)(void *))call_pyobject_call, (void *)&params,
959+
(rb_unblock_function_t *)pycall_interrupt_python_thread, NULL);
960+
}
961+
else {
962+
res = call_pyobject_call(&params);
963+
}
913964

914965
return res;
915966
}
@@ -961,7 +1012,7 @@ pycall_call_python_callable(PyObject *pycallable, int argc, VALUE *argv)
9611012
}
9621013
}
9631014

964-
res = pyobject_call_without_gvl(pycallable, args, kwargs); /* New reference */
1015+
res = pyobject_call(pycallable, args, kwargs); /* New reference */
9651016
if (!res) {
9661017
pycall_pyerror_fetch_and_raise("PyObject_Call in pycall_call_python_callable");
9671018
}
@@ -2185,6 +2236,9 @@ Init_pycall(void)
21852236

21862237
rb_define_module_function(mPyCall, "after_fork", pycall_after_fork, 0);
21872238

2239+
pycall_tls_create(&without_gvl_key);
2240+
rb_define_module_function(mPyCall, "without_gvl", pycall_m_without_gvl, 0);
2241+
21882242
/* PyCall::PyPtr */
21892243

21902244
cPyPtr = rb_define_class_under(mPyCall, "PyPtr", rb_cBasicObject);

ext/pycall/pycall_internal.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,19 @@ extern "C" {
1111
#include <ruby.h>
1212
#include <ruby/encoding.h>
1313
#include <ruby/thread.h>
14+
1415
#include <assert.h>
1516
#include <inttypes.h>
1617
#include <limits.h>
1718

19+
#if defined(_WIN32)
20+
# define PYCALL_THREAD_WIN32
21+
# include <ruby/win32.h>
22+
#elif defined(HAVE_PTHREAD_H)
23+
# define PYCALL_THREAD_PTHREAD
24+
# include <pthread.h>
25+
#endif
26+
1827
#if SIZEOF_LONG == SIZEOF_VOIDP
1928
# define PTR2NUM(x) (LONG2NUM((long)(x)))
2029
# define NUM2PTR(x) ((void*)(NUM2ULONG(x)))
@@ -492,6 +501,23 @@ extern PyTypeObject PyRuby_Type;
492501

493502
PyObject * PyRuby_New(VALUE ruby_object);
494503

504+
/* ==== thread support ==== */
505+
506+
#if defined(PYCALL_THREAD_WIN32)
507+
typedef DWORD pycall_tls_key;
508+
#elif defined(PYCALL_THREAD_PTHREAD)
509+
typedef pthread_key_t pycall_tls_key;
510+
#else
511+
# error "unsupported thread type"
512+
#endif
513+
514+
int pycall_tls_create(pycall_tls_key* tls_key);
515+
void *pycall_tls_get(pycall_tls_key tls_key);
516+
int pycall_tls_set(pycall_tls_key tls_key, void *ptr);
517+
518+
int pycall_without_gvl_p(void);
519+
VALUE pycall_without_gvl(VALUE (* func)(VALUE), VALUE arg);
520+
495521
/* ==== pycall ==== */
496522

497523
typedef struct {

ext/pycall/thread.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#include "pycall_internal.h"
2+
3+
#if defined(PYCALL_THREAD_WIN32)
4+
int pycall_tls_create(pycall_tls_key *tls_key)
5+
{
6+
*tls_key = TlsAlloc();
7+
return *tls_key == TLS_OUT_OF_INDEXES;
8+
}
9+
10+
void *pycall_tls_get(pycall_tls_key tls_key)
11+
{
12+
return TlsGetValue(tls_key);
13+
}
14+
15+
int pycall_tls_set(pycall_tls_key tls_key, void *ptr)
16+
{
17+
return 0 == TlsSetValue(tls_key, ptr);
18+
}
19+
#endif
20+
21+
#if defined(PYCALL_THREAD_PTHREAD)
22+
int pycall_tls_create(pycall_tls_key *tls_key)
23+
{
24+
return pthread_key_create(tls_key, NULL);
25+
}
26+
27+
void *pycall_tls_get(pycall_tls_key tls_key)
28+
{
29+
return pthread_getspecific(tls_key);
30+
}
31+
32+
int pycall_tls_set(pycall_tls_key tls_key, void *ptr)
33+
{
34+
return pthread_setspecific(tls_key, ptr);
35+
}
36+
#endif

spec/pycall/gvl_spec.rb

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,71 @@
11
require 'spec_helper'
22

3-
::RSpec.describe PyCall do
4-
it 'releases GVL during Python C API invocation' do
5-
py_time = PyCall.import_module('time')
6-
7-
mutex = Mutex.new
8-
cv = ConditionVariable.new
9-
cancel = false
10-
counter = Thread.start do
11-
count = 0
12-
mutex.synchronize do
13-
until cancel
14-
count += 1
15-
cv.wait(mutex, 0.1)
3+
RSpec.describe PyCall do
4+
context 'outside of PyCall.without_gvl' do
5+
specify 'PyCall does not releases GVL during Python C API invocation' do
6+
py_time = PyCall.import_module('time')
7+
8+
mutex = Mutex.new
9+
cv = ConditionVariable.new
10+
cancel = false
11+
counter = Thread.start do
12+
count = 0
13+
mutex.synchronize do
14+
until cancel
15+
count += 1
16+
cv.wait(mutex, 0.1)
17+
end
1618
end
19+
count
1720
end
18-
count
19-
end
2021

21-
py_time.sleep(1)
22+
py_time.sleep(1)
2223

23-
mutex.synchronize do
24-
cancel = true
25-
cv.signal
26-
end
24+
mutex.synchronize do
25+
cancel = true
26+
cv.signal
27+
end
2728

28-
expect(counter.value).to be >= 5
29+
expect(counter.value).to be < 5
30+
end
2931
end
3032

31-
it 'acquires GVL during calling Ruby from Python' do
32-
gvl_checker = PyCall::GvlChecker.allocate
33-
ruby_object_test = PyCall.import_module('pycall.ruby_object_test')
34-
expect(ruby_object_test.call_callable(gvl_checker)).to eq(true)
33+
context 'inside of .without_gvl' do
34+
specify 'PyCall releases GVL during Python C API invocation' do
35+
py_time = PyCall.import_module('time')
36+
37+
mutex = Mutex.new
38+
cv = ConditionVariable.new
39+
cancel = false
40+
counter = Thread.start do
41+
count = 0
42+
mutex.synchronize do
43+
until cancel
44+
count += 1
45+
cv.wait(mutex, 0.1)
46+
end
47+
end
48+
count
49+
end
50+
51+
PyCall.without_gvl do
52+
py_time.sleep(1)
53+
end
54+
55+
mutex.synchronize do
56+
cancel = true
57+
cv.signal
58+
end
59+
60+
expect(counter.value).to be >= 5
61+
end
62+
63+
specify 'PyCall acquires GVL during calling Ruby from Python' do
64+
gvl_checker = PyCall::GvlChecker.allocate
65+
ruby_object_test = PyCall.import_module('pycall.ruby_object_test')
66+
PyCall.without_gvl do
67+
expect(ruby_object_test.call_callable(gvl_checker)).to eq(true)
68+
end
69+
end
3570
end
3671
end

0 commit comments

Comments
 (0)