-
-
Notifications
You must be signed in to change notification settings - Fork 30.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh-128942: make arraymodule.c free-thread safe #128943
base: main
Are you sure you want to change the base?
Conversation
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
Requesting a look from @ZeroIntensity. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this! At a glance, don't use goto
with critical sections--it's messier and can be error-prone (IIRC there's problems with it compiling on MSVC too). Instead, create a wrapper function like this:
static PyObject *
array_something(arrayobject *self, PyObject *arg)
{
PyObject *res;
Py_BEGIN_CRITICAL_SECTION(self);
res = array_something_lock_held(); // the actual implementation function
Py_END_CRITICAL_SECTION();
return res;
}
For functions that are defined with the Argument Clinic, you can use @critical_section
to do this even faster. (See what I did for the ssl
module.)
Made requested changes, also made arrayiter safe. Also found a good old-fashioned non-free-threaded bug which probably goes back a long way - if try to Question remaining about need to lock bytes source object in |
Could you separate that out from this PR and submit a dedicated PR for this bug? We might want to backport it. |
Regarding the style nits: please make sure all new code (and in most cases, also changed code) follow PEP-7. |
/*[clinic input] | ||
array.array.frombytes | ||
buffer: Py_buffer | ||
bytes: object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remind me again why we can't use @critical_section
here? It's been a while since I've had a look at these kinds of changes. cc. @ZeroIntensity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just put it on the internal function same as with ins()
, but I can raise the critical sections to the public functions array_array_frombytes
, array_array_insert
and array_array_append
if you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. If Peter is fine with this variant, I'm also fine with it, but please leave the docstring intact. You need this (coupled with a make clinic
):
bytes: object | |
bytes as buffer: object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And maybe you can answer that question for me here, does bytes need to be locked in this case or is it sufficient just to hold the buffer as was the case in the original non-free safe code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this case, I don't think we need a lock_held
function. Just @critical_section
should do.
I'm not sure what the rules are for the buffer protocol in regards to free-threading. Hopefully it's fine to leave bytes
unlocked.
if (value != NULL) { | ||
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(value); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use pre-processor guard instead of runtime guards for these: Py_DEBUG
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole if block gets optimized out when Py_DEBUG
is not defined, but having the #ifdef
doesn't hurt either.
Still would like to know if |
Misc/NEWS.d/next/Library/2025-01-17-13-53-32.gh-issue-128942.DxzaIg.rst
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're getting there! Now, we're overlocking things a bit. This isn't always the rule, but in general you only need to lock something if you want to avoid inconsistent state (making it a "critical section"!).
That, and keep in mind that things that have a _Py
or Py
prefix are generally thread safe themselves and will lock if they need to. You don't need to lock for every call to Python.
/*[clinic input] | ||
array.array.frombytes | ||
buffer: Py_buffer | ||
bytes: object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this case, I don't think we need a lock_held
function. Just @critical_section
should do.
I'm not sure what the rules are for the buffer protocol in regards to free-threading. Hopefully it's fine to leave bytes
unlocked.
@@ -2657,6 +2825,7 @@ array_buffer_getbuf(arrayobject *self, Py_buffer *view, int flags) | |||
return -1; | |||
} | |||
|
|||
Py_BEGIN_CRITICAL_SECTION(self); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should probably just be a lock_held
function.
return 0; | ||
} | ||
|
||
static void | ||
array_buffer_relbuf(arrayobject *self, Py_buffer *view) | ||
{ | ||
self->ob_exports--; | ||
_Py_atomic_add_ssize(&self->ob_exports, -1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will affect the default build, we need an #ifdef Py_GIL_DISABLED
.
@@ -754,7 +760,7 @@ array_richcompare(PyObject *v, PyObject *w, int op) | |||
res = Py_False; | |||
else | |||
res = Py_True; | |||
return Py_NewRef(res); | |||
return res; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks unrelated. Let's try to avoid that for now.
This PR is a work in progress but it currently fixes the crashes mentioned in the related issue so I am sending it up now to elicit feedback to make sure I am on the right path. So far the changes have been made functional but as minimal as possible to facilitate review. The critical sections are fairly broad so optimization may follow if functionality is confirmed. The changes are mostly additions of critical sections to publicly visible methods and slots gotten from the two structures listed below (as well as a few internal functions):
The following public methods/slots have been modified directly:
The following have not been modified but depend for safety on the functions they call:
The following look safe as is:
And the following non-public utility functions have been made safe:
Here are a few questions needing guidance:
Changed clinic sig of
array_array_frombytes()
so I could lock the original source object during op (maybe its a writeable bytearray). Want to prevent modification of data during the operation but not sure this will even do this so not sure is necessary or even desired. Should I remove this and leave just thePyObject_GetBuffer()
for protection? Also for this added a forward declaration ofarray_array_frombytes()
forarray_array_fromfile_impl()
instead of moving stuff around for minimal impact.In
array_new
there may or may not be an initializer object, if there is not then theargs
tuple is used as a stand-in for the critical section lock. Is there a better way to do this?Misc:
array_richcompare
was incrementing refcount ofPy_True
andPy_False
, removed that as they are immortal.The following were larger / slightly more complicated functions to check so should double check
array_array_frombytes
,array_richcompare
,array_ass_subscr
andarray_new
.More work is still needed, like the array iterator and more testing and verification.
array
module is not free-thread safe. #128942