mirror of
https://github.com/python/cpython.git
synced 2025-08-02 16:13:13 +00:00
gh-127065: Make methodcaller thread-safe and re-entrant (GH-127746)
The function `operator.methodcaller` was not thread-safe since the additional of the vectorcall method in gh-89013. In the free threading build the issue is easy to trigger, for the normal build harder. This makes the `methodcaller` safe by: * Replacing the lazy initialization with initialization in the constructor. * Using a stack allocated space for the vectorcall arguments and falling back to `tp_call` for calls with more than 8 arguments.
This commit is contained in:
parent
5a23994a3d
commit
b0f278ff05
4 changed files with 131 additions and 96 deletions
33
Lib/test/test_free_threading/test_methodcaller.py
Normal file
33
Lib/test/test_free_threading/test_methodcaller.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import unittest
|
||||||
|
from threading import Thread
|
||||||
|
from test.support import threading_helper
|
||||||
|
from operator import methodcaller
|
||||||
|
|
||||||
|
|
||||||
|
class TestMethodcaller(unittest.TestCase):
|
||||||
|
def test_methodcaller_threading(self):
|
||||||
|
number_of_threads = 10
|
||||||
|
size = 4_000
|
||||||
|
|
||||||
|
mc = methodcaller("append", 2)
|
||||||
|
|
||||||
|
def work(mc, l, ii):
|
||||||
|
for _ in range(ii):
|
||||||
|
mc(l)
|
||||||
|
|
||||||
|
worker_threads = []
|
||||||
|
lists = []
|
||||||
|
for ii in range(number_of_threads):
|
||||||
|
l = []
|
||||||
|
lists.append(l)
|
||||||
|
worker_threads.append(Thread(target=work, args=[mc, l, size]))
|
||||||
|
for t in worker_threads:
|
||||||
|
t.start()
|
||||||
|
for t in worker_threads:
|
||||||
|
t.join()
|
||||||
|
for l in lists:
|
||||||
|
assert len(l) == size
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
|
@ -482,6 +482,8 @@ class OperatorTestCase:
|
||||||
return f
|
return f
|
||||||
def baz(*args, **kwds):
|
def baz(*args, **kwds):
|
||||||
return kwds['name'], kwds['self']
|
return kwds['name'], kwds['self']
|
||||||
|
def return_arguments(self, *args, **kwds):
|
||||||
|
return args, kwds
|
||||||
a = A()
|
a = A()
|
||||||
f = operator.methodcaller('foo')
|
f = operator.methodcaller('foo')
|
||||||
self.assertRaises(IndexError, f, a)
|
self.assertRaises(IndexError, f, a)
|
||||||
|
@ -498,6 +500,17 @@ class OperatorTestCase:
|
||||||
f = operator.methodcaller('baz', name='spam', self='eggs')
|
f = operator.methodcaller('baz', name='spam', self='eggs')
|
||||||
self.assertEqual(f(a), ('spam', 'eggs'))
|
self.assertEqual(f(a), ('spam', 'eggs'))
|
||||||
|
|
||||||
|
many_positional_arguments = tuple(range(10))
|
||||||
|
many_kw_arguments = dict(zip('abcdefghij', range(10)))
|
||||||
|
f = operator.methodcaller('return_arguments', *many_positional_arguments)
|
||||||
|
self.assertEqual(f(a), (many_positional_arguments, {}))
|
||||||
|
|
||||||
|
f = operator.methodcaller('return_arguments', **many_kw_arguments)
|
||||||
|
self.assertEqual(f(a), ((), many_kw_arguments))
|
||||||
|
|
||||||
|
f = operator.methodcaller('return_arguments', *many_positional_arguments, **many_kw_arguments)
|
||||||
|
self.assertEqual(f(a), (many_positional_arguments, many_kw_arguments))
|
||||||
|
|
||||||
def test_inplace(self):
|
def test_inplace(self):
|
||||||
operator = self.module
|
operator = self.module
|
||||||
class C(object):
|
class C(object):
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Make :func:`operator.methodcaller` thread-safe and re-entrant safe.
|
|
@ -1595,78 +1595,75 @@ static PyType_Spec attrgetter_type_spec = {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject_HEAD
|
PyObject_HEAD
|
||||||
PyObject *name;
|
PyObject *name;
|
||||||
PyObject *xargs; // reference to arguments passed in constructor
|
PyObject *args;
|
||||||
PyObject *kwds;
|
PyObject *kwds;
|
||||||
PyObject **vectorcall_args; /* Borrowed references */
|
PyObject *vectorcall_args;
|
||||||
PyObject *vectorcall_kwnames;
|
PyObject *vectorcall_kwnames;
|
||||||
vectorcallfunc vectorcall;
|
vectorcallfunc vectorcall;
|
||||||
} methodcallerobject;
|
} methodcallerobject;
|
||||||
|
|
||||||
#ifndef Py_GIL_DISABLED
|
|
||||||
static int _methodcaller_initialize_vectorcall(methodcallerobject* mc)
|
|
||||||
{
|
|
||||||
PyObject* args = mc->xargs;
|
|
||||||
PyObject* kwds = mc->kwds;
|
|
||||||
|
|
||||||
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
|
|
||||||
assert(nargs > 0);
|
|
||||||
mc->vectorcall_args = PyMem_Calloc(
|
|
||||||
nargs + (kwds ? PyDict_Size(kwds) : 0),
|
|
||||||
sizeof(PyObject*));
|
|
||||||
if (!mc->vectorcall_args) {
|
|
||||||
PyErr_NoMemory();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
/* The first item of vectorcall_args will be filled with obj later */
|
|
||||||
if (nargs > 1) {
|
|
||||||
memcpy(mc->vectorcall_args, PySequence_Fast_ITEMS(args),
|
|
||||||
nargs * sizeof(PyObject*));
|
|
||||||
}
|
|
||||||
if (kwds) {
|
|
||||||
const Py_ssize_t nkwds = PyDict_Size(kwds);
|
|
||||||
|
|
||||||
mc->vectorcall_kwnames = PyTuple_New(nkwds);
|
|
||||||
if (!mc->vectorcall_kwnames) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
Py_ssize_t i = 0, ppos = 0;
|
|
||||||
PyObject* key, * value;
|
|
||||||
while (PyDict_Next(kwds, &ppos, &key, &value)) {
|
|
||||||
PyTuple_SET_ITEM(mc->vectorcall_kwnames, i, Py_NewRef(key));
|
|
||||||
mc->vectorcall_args[nargs + i] = value; // borrowed reference
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
mc->vectorcall_kwnames = NULL;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#define _METHODCALLER_MAX_ARGS 8
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
methodcaller_vectorcall(
|
methodcaller_vectorcall(methodcallerobject *mc, PyObject *const *args,
|
||||||
methodcallerobject *mc, PyObject *const *args, size_t nargsf, PyObject* kwnames)
|
size_t nargsf, PyObject* kwnames)
|
||||||
{
|
{
|
||||||
if (!_PyArg_CheckPositional("methodcaller", PyVectorcall_NARGS(nargsf), 1, 1)
|
if (!_PyArg_CheckPositional("methodcaller", PyVectorcall_NARGS(nargsf), 1, 1)
|
||||||
|| !_PyArg_NoKwnames("methodcaller", kwnames)) {
|
|| !_PyArg_NoKwnames("methodcaller", kwnames)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (mc->vectorcall_args == NULL) {
|
assert(mc->vectorcall_args != NULL);
|
||||||
if (_methodcaller_initialize_vectorcall(mc) < 0) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(mc->vectorcall_args != 0);
|
PyObject *tmp_args[_METHODCALLER_MAX_ARGS];
|
||||||
mc->vectorcall_args[0] = args[0];
|
tmp_args[0] = args[0];
|
||||||
return PyObject_VectorcallMethod(
|
assert(1 + PyTuple_GET_SIZE(mc->vectorcall_args) <= _METHODCALLER_MAX_ARGS);
|
||||||
mc->name, mc->vectorcall_args,
|
memcpy(tmp_args + 1, _PyTuple_ITEMS(mc->vectorcall_args), sizeof(PyObject *) * PyTuple_GET_SIZE(mc->vectorcall_args));
|
||||||
(PyTuple_GET_SIZE(mc->xargs)) | PY_VECTORCALL_ARGUMENTS_OFFSET,
|
|
||||||
|
return PyObject_VectorcallMethod(mc->name, tmp_args,
|
||||||
|
(1 + PyTuple_GET_SIZE(mc->args)) | PY_VECTORCALL_ARGUMENTS_OFFSET,
|
||||||
mc->vectorcall_kwnames);
|
mc->vectorcall_kwnames);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
|
static int
|
||||||
|
_methodcaller_initialize_vectorcall(methodcallerobject* mc)
|
||||||
|
{
|
||||||
|
PyObject* args = mc->args;
|
||||||
|
PyObject* kwds = mc->kwds;
|
||||||
|
|
||||||
|
if (kwds && PyDict_Size(kwds)) {
|
||||||
|
PyObject *values = PyDict_Values(kwds);
|
||||||
|
if (!values) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
PyObject *values_tuple = PySequence_Tuple(values);
|
||||||
|
Py_DECREF(values);
|
||||||
|
if (!values_tuple) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (PyTuple_GET_SIZE(args)) {
|
||||||
|
mc->vectorcall_args = PySequence_Concat(args, values_tuple);
|
||||||
|
Py_DECREF(values_tuple);
|
||||||
|
if (mc->vectorcall_args == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
mc->vectorcall_args = values_tuple;
|
||||||
|
}
|
||||||
|
mc->vectorcall_kwnames = PySequence_Tuple(kwds);
|
||||||
|
if (!mc->vectorcall_kwnames) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
mc->vectorcall_args = Py_NewRef(args);
|
||||||
|
mc->vectorcall_kwnames = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
mc->vectorcall = (vectorcallfunc)methodcaller_vectorcall;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* AC 3.5: variable number of arguments, not currently support by AC */
|
/* AC 3.5: variable number of arguments, not currently support by AC */
|
||||||
static PyObject *
|
static PyObject *
|
||||||
|
@ -1694,25 +1691,30 @@ methodcaller_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
|
||||||
if (mc == NULL) {
|
if (mc == NULL) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
mc->vectorcall = NULL;
|
||||||
|
mc->vectorcall_args = NULL;
|
||||||
|
mc->vectorcall_kwnames = NULL;
|
||||||
|
mc->kwds = Py_XNewRef(kwds);
|
||||||
|
|
||||||
Py_INCREF(name);
|
Py_INCREF(name);
|
||||||
PyInterpreterState *interp = _PyInterpreterState_GET();
|
PyInterpreterState *interp = _PyInterpreterState_GET();
|
||||||
_PyUnicode_InternMortal(interp, &name);
|
_PyUnicode_InternMortal(interp, &name);
|
||||||
mc->name = name;
|
mc->name = name;
|
||||||
|
|
||||||
mc->xargs = Py_XNewRef(args); // allows us to use borrowed references
|
mc->args = PyTuple_GetSlice(args, 1, PyTuple_GET_SIZE(args));
|
||||||
mc->kwds = Py_XNewRef(kwds);
|
if (mc->args == NULL) {
|
||||||
mc->vectorcall_args = 0;
|
Py_DECREF(mc);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_ssize_t vectorcall_size = PyTuple_GET_SIZE(args)
|
||||||
#ifdef Py_GIL_DISABLED
|
+ (kwds ? PyDict_Size(kwds) : 0);
|
||||||
// gh-127065: The current implementation of methodcaller_vectorcall
|
if (vectorcall_size < (_METHODCALLER_MAX_ARGS)) {
|
||||||
// is not thread-safe because it modifies the `vectorcall_args` array,
|
if (_methodcaller_initialize_vectorcall(mc) < 0) {
|
||||||
// which is shared across calls.
|
Py_DECREF(mc);
|
||||||
mc->vectorcall = NULL;
|
return NULL;
|
||||||
#else
|
}
|
||||||
mc->vectorcall = (vectorcallfunc)methodcaller_vectorcall;
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
PyObject_GC_Track(mc);
|
PyObject_GC_Track(mc);
|
||||||
return (PyObject *)mc;
|
return (PyObject *)mc;
|
||||||
|
@ -1722,13 +1724,10 @@ static void
|
||||||
methodcaller_clear(methodcallerobject *mc)
|
methodcaller_clear(methodcallerobject *mc)
|
||||||
{
|
{
|
||||||
Py_CLEAR(mc->name);
|
Py_CLEAR(mc->name);
|
||||||
Py_CLEAR(mc->xargs);
|
Py_CLEAR(mc->args);
|
||||||
Py_CLEAR(mc->kwds);
|
Py_CLEAR(mc->kwds);
|
||||||
if (mc->vectorcall_args != NULL) {
|
Py_CLEAR(mc->vectorcall_args);
|
||||||
PyMem_Free(mc->vectorcall_args);
|
Py_CLEAR(mc->vectorcall_kwnames);
|
||||||
mc->vectorcall_args = 0;
|
|
||||||
Py_CLEAR(mc->vectorcall_kwnames);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -1745,8 +1744,10 @@ static int
|
||||||
methodcaller_traverse(methodcallerobject *mc, visitproc visit, void *arg)
|
methodcaller_traverse(methodcallerobject *mc, visitproc visit, void *arg)
|
||||||
{
|
{
|
||||||
Py_VISIT(mc->name);
|
Py_VISIT(mc->name);
|
||||||
Py_VISIT(mc->xargs);
|
Py_VISIT(mc->args);
|
||||||
Py_VISIT(mc->kwds);
|
Py_VISIT(mc->kwds);
|
||||||
|
Py_VISIT(mc->vectorcall_args);
|
||||||
|
Py_VISIT(mc->vectorcall_kwnames);
|
||||||
Py_VISIT(Py_TYPE(mc));
|
Py_VISIT(Py_TYPE(mc));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1765,15 +1766,7 @@ methodcaller_call(methodcallerobject *mc, PyObject *args, PyObject *kw)
|
||||||
if (method == NULL)
|
if (method == NULL)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
result = PyObject_Call(method, mc->args, mc->kwds);
|
||||||
PyObject *cargs = PyTuple_GetSlice(mc->xargs, 1, PyTuple_GET_SIZE(mc->xargs));
|
|
||||||
if (cargs == NULL) {
|
|
||||||
Py_DECREF(method);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = PyObject_Call(method, cargs, mc->kwds);
|
|
||||||
Py_DECREF(cargs);
|
|
||||||
Py_DECREF(method);
|
Py_DECREF(method);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -1791,7 +1784,7 @@ methodcaller_repr(methodcallerobject *mc)
|
||||||
}
|
}
|
||||||
|
|
||||||
numkwdargs = mc->kwds != NULL ? PyDict_GET_SIZE(mc->kwds) : 0;
|
numkwdargs = mc->kwds != NULL ? PyDict_GET_SIZE(mc->kwds) : 0;
|
||||||
numposargs = PyTuple_GET_SIZE(mc->xargs) - 1;
|
numposargs = PyTuple_GET_SIZE(mc->args);
|
||||||
numtotalargs = numposargs + numkwdargs;
|
numtotalargs = numposargs + numkwdargs;
|
||||||
|
|
||||||
if (numtotalargs == 0) {
|
if (numtotalargs == 0) {
|
||||||
|
@ -1807,7 +1800,7 @@ methodcaller_repr(methodcallerobject *mc)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i = 0; i < numposargs; ++i) {
|
for (i = 0; i < numposargs; ++i) {
|
||||||
PyObject *onerepr = PyObject_Repr(PyTuple_GET_ITEM(mc->xargs, i+1));
|
PyObject *onerepr = PyObject_Repr(PyTuple_GET_ITEM(mc->args, i));
|
||||||
if (onerepr == NULL)
|
if (onerepr == NULL)
|
||||||
goto done;
|
goto done;
|
||||||
PyTuple_SET_ITEM(argreprs, i, onerepr);
|
PyTuple_SET_ITEM(argreprs, i, onerepr);
|
||||||
|
@ -1859,14 +1852,14 @@ methodcaller_reduce(methodcallerobject *mc, PyObject *Py_UNUSED(ignored))
|
||||||
{
|
{
|
||||||
if (!mc->kwds || PyDict_GET_SIZE(mc->kwds) == 0) {
|
if (!mc->kwds || PyDict_GET_SIZE(mc->kwds) == 0) {
|
||||||
Py_ssize_t i;
|
Py_ssize_t i;
|
||||||
Py_ssize_t newarg_size = PyTuple_GET_SIZE(mc->xargs);
|
Py_ssize_t callargcount = PyTuple_GET_SIZE(mc->args);
|
||||||
PyObject *newargs = PyTuple_New(newarg_size);
|
PyObject *newargs = PyTuple_New(1 + callargcount);
|
||||||
if (newargs == NULL)
|
if (newargs == NULL)
|
||||||
return NULL;
|
return NULL;
|
||||||
PyTuple_SET_ITEM(newargs, 0, Py_NewRef(mc->name));
|
PyTuple_SET_ITEM(newargs, 0, Py_NewRef(mc->name));
|
||||||
for (i = 1; i < newarg_size; ++i) {
|
for (i = 0; i < callargcount; ++i) {
|
||||||
PyObject *arg = PyTuple_GET_ITEM(mc->xargs, i);
|
PyObject *arg = PyTuple_GET_ITEM(mc->args, i);
|
||||||
PyTuple_SET_ITEM(newargs, i, Py_NewRef(arg));
|
PyTuple_SET_ITEM(newargs, i + 1, Py_NewRef(arg));
|
||||||
}
|
}
|
||||||
return Py_BuildValue("ON", Py_TYPE(mc), newargs);
|
return Py_BuildValue("ON", Py_TYPE(mc), newargs);
|
||||||
}
|
}
|
||||||
|
@ -1884,12 +1877,7 @@ methodcaller_reduce(methodcallerobject *mc, PyObject *Py_UNUSED(ignored))
|
||||||
constructor = PyObject_VectorcallDict(partial, newargs, 2, mc->kwds);
|
constructor = PyObject_VectorcallDict(partial, newargs, 2, mc->kwds);
|
||||||
|
|
||||||
Py_DECREF(partial);
|
Py_DECREF(partial);
|
||||||
PyObject *args = PyTuple_GetSlice(mc->xargs, 1, PyTuple_GET_SIZE(mc->xargs));
|
return Py_BuildValue("NO", constructor, mc->args);
|
||||||
if (!args) {
|
|
||||||
Py_DECREF(constructor);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return Py_BuildValue("NO", constructor, args);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue