-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
gh-133931: Introduce _PyObject_XSetRefDelayed to replace Py_XSETREF #134377
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
base: main
Are you sure you want to change the base?
Changes from all commits
09baa04
1ee0825
753df7a
a168de0
e7f6d01
6287322
12a7016
5f42224
388f0ca
8220367
c82e0fb
f99c8f1
5dc24a3
99d2718
7dc6892
e65eb1f
4dbb0a7
d0ba9c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import concurrent.futures | ||
import unittest | ||
from threading import Barrier | ||
from unittest import TestCase | ||
import random | ||
import time | ||
|
||
from test.support import threading_helper, Py_GIL_DISABLED | ||
|
||
threading_helper.requires_working_threading(module=True) | ||
|
||
|
||
def random_sleep(): | ||
delay_us = random.randint(50, 100) | ||
time.sleep(delay_us * 1e-6) | ||
|
||
def random_string(): | ||
return ''.join(random.choice('0123456789ABCDEF') for _ in range(10)) | ||
|
||
def set_gen_name(g, b): | ||
b.wait() | ||
random_sleep() | ||
g.__name__ = random_string() | ||
return g.__name__ | ||
|
||
def set_gen_qualname(g, b): | ||
b.wait() | ||
random_sleep() | ||
g.__qualname__ = random_string() | ||
return g.__qualname__ | ||
|
||
|
||
@unittest.skipUnless(Py_GIL_DISABLED, "Enable only in FT build") | ||
class TestFTGenerators(TestCase): | ||
NUM_THREADS = 4 | ||
|
||
def concurrent_write_with_func(self, func): | ||
gen = (x for x in range(42)) | ||
for j in range(1000): | ||
with concurrent.futures.ThreadPoolExecutor(max_workers=self.NUM_THREADS) as executor: | ||
b = Barrier(self.NUM_THREADS) | ||
futures = {executor.submit(func, gen, b): i for i in range(self.NUM_THREADS)} | ||
for fut in concurrent.futures.as_completed(futures): | ||
gen_name = fut.result() | ||
self.assertEqual(len(gen_name), 10) | ||
|
||
def test_concurrent_write(self): | ||
with self.subTest(func=set_gen_name): | ||
self.concurrent_write_with_func(func=set_gen_name) | ||
with self.subTest(func=set_gen_qualname): | ||
self.concurrent_write_with_func(func=set_gen_qualname) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
#include "pycore_gc.h" // _PyGC_CLEAR_FINALIZED() | ||
#include "pycore_genobject.h" // _PyGen_SetStopIterationValue() | ||
#include "pycore_interpframe.h" // _PyFrame_GetCode() | ||
#include "pycore_pymem.h" // _PyObject_XSetRefDelayed() | ||
#include "pycore_modsupport.h" // _PyArg_CheckPositional() | ||
#include "pycore_object.h" // _PyObject_GC_UNTRACK() | ||
#include "pycore_opcode_utils.h" // RESUME_AFTER_YIELD_FROM | ||
|
@@ -704,7 +705,8 @@ static PyObject * | |
gen_get_name(PyObject *self, void *Py_UNUSED(ignored)) | ||
{ | ||
PyGenObject *op = _PyGen_CAST(self); | ||
return Py_NewRef(op->gi_name); | ||
PyObject *name = FT_ATOMIC_LOAD_PTR_RELAXED(op->gi_name); | ||
return Py_NewRef(name); | ||
} | ||
|
||
static int | ||
|
@@ -718,15 +720,19 @@ gen_set_name(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)) | |
"__name__ must be set to a string object"); | ||
return -1; | ||
} | ||
Py_XSETREF(op->gi_name, Py_NewRef(value)); | ||
Py_BEGIN_CRITICAL_SECTION(self); | ||
// To prevent use-after-free from other threads that reference the gi_name. | ||
_PyObject_XSetRefDelayed(&op->gi_name, Py_NewRef(value)); | ||
corona10 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Py_END_CRITICAL_SECTION(); | ||
return 0; | ||
} | ||
|
||
static PyObject * | ||
gen_get_qualname(PyObject *self, void *Py_UNUSED(ignored)) | ||
{ | ||
PyGenObject *op = _PyGen_CAST(self); | ||
return Py_NewRef(op->gi_qualname); | ||
PyObject *qualname = FT_ATOMIC_LOAD_PTR_RELAXED(op->gi_qualname); | ||
return Py_NewRef(qualname); | ||
} | ||
|
||
static int | ||
|
@@ -740,7 +746,10 @@ gen_set_qualname(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)) | |
"__qualname__ must be set to a string object"); | ||
return -1; | ||
} | ||
Py_XSETREF(op->gi_qualname, Py_NewRef(value)); | ||
Py_BEGIN_CRITICAL_SECTION(self); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind adding a comment explaining why XDecRefDelayed() is needed here? The qualname should be a simple Python str object, no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even with simple str objects there can be a crash w/o critical session. What exact way of fixing you're proposing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not proposing anything, I don't understand well the purpose of XDecRefDelayed(). That's why I'm asking for a comment :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO, it will be great to have some best practices for free-threading build in InternalDocs. This case seems like a standard one, and some standard solutions will be great. For this exact case we just need some kind of locking for synchronization reading/writing. And there's one way for this. |
||
// To prevent use-after-free from other threads that reference the gi_qualname. | ||
_PyObject_XSetRefDelayed(&op->gi_qualname, Py_NewRef(value)); | ||
Py_END_CRITICAL_SECTION(); | ||
return 0; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3967,13 +3967,8 @@ _PyObject_SetDict(PyObject *obj, PyObject *value) | |
return -1; | ||
} | ||
Py_BEGIN_CRITICAL_SECTION(obj); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind adding a comment explaining why XDecRefDelayed() is needed here? |
||
PyObject *olddict = *dictptr; | ||
FT_ATOMIC_STORE_PTR_RELEASE(*dictptr, Py_NewRef(value)); | ||
#ifdef Py_GIL_DISABLED | ||
_PyObject_XDecRefDelayed(olddict); | ||
#else | ||
Py_XDECREF(olddict); | ||
#endif | ||
// To prevent use-after-free from other threads that reference the __dict__ | ||
_PyObject_XSetRefDelayed(dictptr, Py_NewRef(value)); | ||
Py_END_CRITICAL_SECTION(); | ||
return 0; | ||
} | ||
|
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.
Would you mind adding a comment explaining why XDecRefDelayed() is needed here? The name should be a simple Python str object, no?