Skip to content

Commit 05af03a

Browse files
eendebakptsrinivasreddy
authored andcommitted
pythongh-127065: Make methodcaller thread-safe and re-entrant (pythonGH-127746)
The function `operator.methodcaller` was not thread-safe since the additional of the vectorcall method in pythongh-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.
1 parent 44e5b2b commit 05af03a

File tree

4 files changed

+131
-96
lines changed

4 files changed

+131
-96
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import unittest
2+
from threading import Thread
3+
from test.support import threading_helper
4+
from operator import methodcaller
5+
6+
7+
class TestMethodcaller(unittest.TestCase):
8+
def test_methodcaller_threading(self):
9+
number_of_threads = 10
10+
size = 4_000
11+
12+
mc = methodcaller("append", 2)
13+
14+
def work(mc, l, ii):
15+
for _ in range(ii):
16+
mc(l)
17+
18+
worker_threads = []
19+
lists = []
20+
for ii in range(number_of_threads):
21+
l = []
22+
lists.append(l)
23+
worker_threads.append(Thread(target=work, args=[mc, l, size]))
24+
for t in worker_threads:
25+
t.start()
26+
for t in worker_threads:
27+
t.join()
28+
for l in lists:
29+
assert len(l) == size
30+
31+
32+
if __name__ == "__main__":
33+
unittest.main()

Lib/test/test_operator.py

+13
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,8 @@ def bar(self, f=42):
482482
return f
483483
def baz(*args, **kwds):
484484
return kwds['name'], kwds['self']
485+
def return_arguments(self, *args, **kwds):
486+
return args, kwds
485487
a = A()
486488
f = operator.methodcaller('foo')
487489
self.assertRaises(IndexError, f, a)
@@ -498,6 +500,17 @@ def baz(*args, **kwds):
498500
f = operator.methodcaller('baz', name='spam', self='eggs')
499501
self.assertEqual(f(a), ('spam', 'eggs'))
500502

503+
many_positional_arguments = tuple(range(10))
504+
many_kw_arguments = dict(zip('abcdefghij', range(10)))
505+
f = operator.methodcaller('return_arguments', *many_positional_arguments)
506+
self.assertEqual(f(a), (many_positional_arguments, {}))
507+
508+
f = operator.methodcaller('return_arguments', **many_kw_arguments)
509+
self.assertEqual(f(a), ((), many_kw_arguments))
510+
511+
f = operator.methodcaller('return_arguments', *many_positional_arguments, **many_kw_arguments)
512+
self.assertEqual(f(a), (many_positional_arguments, many_kw_arguments))
513+
501514
def test_inplace(self):
502515
operator = self.module
503516
class C(object):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make :func:`operator.methodcaller` thread-safe and re-entrant safe.

Modules/_operator.c

+84-96
Original file line numberDiff line numberDiff line change
@@ -1595,78 +1595,75 @@ static PyType_Spec attrgetter_type_spec = {
15951595
typedef struct {
15961596
PyObject_HEAD
15971597
PyObject *name;
1598-
PyObject *xargs; // reference to arguments passed in constructor
1598+
PyObject *args;
15991599
PyObject *kwds;
1600-
PyObject **vectorcall_args; /* Borrowed references */
1600+
PyObject *vectorcall_args;
16011601
PyObject *vectorcall_kwnames;
16021602
vectorcallfunc vectorcall;
16031603
} methodcallerobject;
16041604

1605-
#ifndef Py_GIL_DISABLED
1606-
static int _methodcaller_initialize_vectorcall(methodcallerobject* mc)
1607-
{
1608-
PyObject* args = mc->xargs;
1609-
PyObject* kwds = mc->kwds;
1610-
1611-
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
1612-
assert(nargs > 0);
1613-
mc->vectorcall_args = PyMem_Calloc(
1614-
nargs + (kwds ? PyDict_Size(kwds) : 0),
1615-
sizeof(PyObject*));
1616-
if (!mc->vectorcall_args) {
1617-
PyErr_NoMemory();
1618-
return -1;
1619-
}
1620-
/* The first item of vectorcall_args will be filled with obj later */
1621-
if (nargs > 1) {
1622-
memcpy(mc->vectorcall_args, PySequence_Fast_ITEMS(args),
1623-
nargs * sizeof(PyObject*));
1624-
}
1625-
if (kwds) {
1626-
const Py_ssize_t nkwds = PyDict_Size(kwds);
1627-
1628-
mc->vectorcall_kwnames = PyTuple_New(nkwds);
1629-
if (!mc->vectorcall_kwnames) {
1630-
return -1;
1631-
}
1632-
Py_ssize_t i = 0, ppos = 0;
1633-
PyObject* key, * value;
1634-
while (PyDict_Next(kwds, &ppos, &key, &value)) {
1635-
PyTuple_SET_ITEM(mc->vectorcall_kwnames, i, Py_NewRef(key));
1636-
mc->vectorcall_args[nargs + i] = value; // borrowed reference
1637-
++i;
1638-
}
1639-
}
1640-
else {
1641-
mc->vectorcall_kwnames = NULL;
1642-
}
1643-
return 1;
1644-
}
16451605

1606+
#define _METHODCALLER_MAX_ARGS 8
16461607

16471608
static PyObject *
1648-
methodcaller_vectorcall(
1649-
methodcallerobject *mc, PyObject *const *args, size_t nargsf, PyObject* kwnames)
1609+
methodcaller_vectorcall(methodcallerobject *mc, PyObject *const *args,
1610+
size_t nargsf, PyObject* kwnames)
16501611
{
16511612
if (!_PyArg_CheckPositional("methodcaller", PyVectorcall_NARGS(nargsf), 1, 1)
16521613
|| !_PyArg_NoKwnames("methodcaller", kwnames)) {
16531614
return NULL;
16541615
}
1655-
if (mc->vectorcall_args == NULL) {
1656-
if (_methodcaller_initialize_vectorcall(mc) < 0) {
1657-
return NULL;
1658-
}
1659-
}
1616+
assert(mc->vectorcall_args != NULL);
1617+
1618+
PyObject *tmp_args[_METHODCALLER_MAX_ARGS];
1619+
tmp_args[0] = args[0];
1620+
assert(1 + PyTuple_GET_SIZE(mc->vectorcall_args) <= _METHODCALLER_MAX_ARGS);
1621+
memcpy(tmp_args + 1, _PyTuple_ITEMS(mc->vectorcall_args), sizeof(PyObject *) * PyTuple_GET_SIZE(mc->vectorcall_args));
16601622

1661-
assert(mc->vectorcall_args != 0);
1662-
mc->vectorcall_args[0] = args[0];
1663-
return PyObject_VectorcallMethod(
1664-
mc->name, mc->vectorcall_args,
1665-
(PyTuple_GET_SIZE(mc->xargs)) | PY_VECTORCALL_ARGUMENTS_OFFSET,
1623+
return PyObject_VectorcallMethod(mc->name, tmp_args,
1624+
(1 + PyTuple_GET_SIZE(mc->args)) | PY_VECTORCALL_ARGUMENTS_OFFSET,
16661625
mc->vectorcall_kwnames);
16671626
}
1668-
#endif
16691627

1628+
static int
1629+
_methodcaller_initialize_vectorcall(methodcallerobject* mc)
1630+
{
1631+
PyObject* args = mc->args;
1632+
PyObject* kwds = mc->kwds;
1633+
1634+
if (kwds && PyDict_Size(kwds)) {
1635+
PyObject *values = PyDict_Values(kwds);
1636+
if (!values) {
1637+
return -1;
1638+
}
1639+
PyObject *values_tuple = PySequence_Tuple(values);
1640+
Py_DECREF(values);
1641+
if (!values_tuple) {
1642+
return -1;
1643+
}
1644+
if (PyTuple_GET_SIZE(args)) {
1645+
mc->vectorcall_args = PySequence_Concat(args, values_tuple);
1646+
Py_DECREF(values_tuple);
1647+
if (mc->vectorcall_args == NULL) {
1648+
return -1;
1649+
}
1650+
}
1651+
else {
1652+
mc->vectorcall_args = values_tuple;
1653+
}
1654+
mc->vectorcall_kwnames = PySequence_Tuple(kwds);
1655+
if (!mc->vectorcall_kwnames) {
1656+
return -1;
1657+
}
1658+
}
1659+
else {
1660+
mc->vectorcall_args = Py_NewRef(args);
1661+
mc->vectorcall_kwnames = NULL;
1662+
}
1663+
1664+
mc->vectorcall = (vectorcallfunc)methodcaller_vectorcall;
1665+
return 0;
1666+
}
16701667

16711668
/* AC 3.5: variable number of arguments, not currently support by AC */
16721669
static PyObject *
@@ -1694,25 +1691,30 @@ methodcaller_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
16941691
if (mc == NULL) {
16951692
return NULL;
16961693
}
1694+
mc->vectorcall = NULL;
1695+
mc->vectorcall_args = NULL;
1696+
mc->vectorcall_kwnames = NULL;
1697+
mc->kwds = Py_XNewRef(kwds);
16971698

16981699
Py_INCREF(name);
16991700
PyInterpreterState *interp = _PyInterpreterState_GET();
17001701
_PyUnicode_InternMortal(interp, &name);
17011702
mc->name = name;
17021703

1703-
mc->xargs = Py_XNewRef(args); // allows us to use borrowed references
1704-
mc->kwds = Py_XNewRef(kwds);
1705-
mc->vectorcall_args = 0;
1706-
1704+
mc->args = PyTuple_GetSlice(args, 1, PyTuple_GET_SIZE(args));
1705+
if (mc->args == NULL) {
1706+
Py_DECREF(mc);
1707+
return NULL;
1708+
}
17071709

1708-
#ifdef Py_GIL_DISABLED
1709-
// gh-127065: The current implementation of methodcaller_vectorcall
1710-
// is not thread-safe because it modifies the `vectorcall_args` array,
1711-
// which is shared across calls.
1712-
mc->vectorcall = NULL;
1713-
#else
1714-
mc->vectorcall = (vectorcallfunc)methodcaller_vectorcall;
1715-
#endif
1710+
Py_ssize_t vectorcall_size = PyTuple_GET_SIZE(args)
1711+
+ (kwds ? PyDict_Size(kwds) : 0);
1712+
if (vectorcall_size < (_METHODCALLER_MAX_ARGS)) {
1713+
if (_methodcaller_initialize_vectorcall(mc) < 0) {
1714+
Py_DECREF(mc);
1715+
return NULL;
1716+
}
1717+
}
17161718

17171719
PyObject_GC_Track(mc);
17181720
return (PyObject *)mc;
@@ -1722,13 +1724,10 @@ static void
17221724
methodcaller_clear(methodcallerobject *mc)
17231725
{
17241726
Py_CLEAR(mc->name);
1725-
Py_CLEAR(mc->xargs);
1727+
Py_CLEAR(mc->args);
17261728
Py_CLEAR(mc->kwds);
1727-
if (mc->vectorcall_args != NULL) {
1728-
PyMem_Free(mc->vectorcall_args);
1729-
mc->vectorcall_args = 0;
1730-
Py_CLEAR(mc->vectorcall_kwnames);
1731-
}
1729+
Py_CLEAR(mc->vectorcall_args);
1730+
Py_CLEAR(mc->vectorcall_kwnames);
17321731
}
17331732

17341733
static void
@@ -1745,8 +1744,10 @@ static int
17451744
methodcaller_traverse(methodcallerobject *mc, visitproc visit, void *arg)
17461745
{
17471746
Py_VISIT(mc->name);
1748-
Py_VISIT(mc->xargs);
1747+
Py_VISIT(mc->args);
17491748
Py_VISIT(mc->kwds);
1749+
Py_VISIT(mc->vectorcall_args);
1750+
Py_VISIT(mc->vectorcall_kwnames);
17501751
Py_VISIT(Py_TYPE(mc));
17511752
return 0;
17521753
}
@@ -1765,15 +1766,7 @@ methodcaller_call(methodcallerobject *mc, PyObject *args, PyObject *kw)
17651766
if (method == NULL)
17661767
return NULL;
17671768

1768-
1769-
PyObject *cargs = PyTuple_GetSlice(mc->xargs, 1, PyTuple_GET_SIZE(mc->xargs));
1770-
if (cargs == NULL) {
1771-
Py_DECREF(method);
1772-
return NULL;
1773-
}
1774-
1775-
result = PyObject_Call(method, cargs, mc->kwds);
1776-
Py_DECREF(cargs);
1769+
result = PyObject_Call(method, mc->args, mc->kwds);
17771770
Py_DECREF(method);
17781771
return result;
17791772
}
@@ -1791,7 +1784,7 @@ methodcaller_repr(methodcallerobject *mc)
17911784
}
17921785

17931786
numkwdargs = mc->kwds != NULL ? PyDict_GET_SIZE(mc->kwds) : 0;
1794-
numposargs = PyTuple_GET_SIZE(mc->xargs) - 1;
1787+
numposargs = PyTuple_GET_SIZE(mc->args);
17951788
numtotalargs = numposargs + numkwdargs;
17961789

17971790
if (numtotalargs == 0) {
@@ -1807,7 +1800,7 @@ methodcaller_repr(methodcallerobject *mc)
18071800
}
18081801

18091802
for (i = 0; i < numposargs; ++i) {
1810-
PyObject *onerepr = PyObject_Repr(PyTuple_GET_ITEM(mc->xargs, i+1));
1803+
PyObject *onerepr = PyObject_Repr(PyTuple_GET_ITEM(mc->args, i));
18111804
if (onerepr == NULL)
18121805
goto done;
18131806
PyTuple_SET_ITEM(argreprs, i, onerepr);
@@ -1859,14 +1852,14 @@ methodcaller_reduce(methodcallerobject *mc, PyObject *Py_UNUSED(ignored))
18591852
{
18601853
if (!mc->kwds || PyDict_GET_SIZE(mc->kwds) == 0) {
18611854
Py_ssize_t i;
1862-
Py_ssize_t newarg_size = PyTuple_GET_SIZE(mc->xargs);
1863-
PyObject *newargs = PyTuple_New(newarg_size);
1855+
Py_ssize_t callargcount = PyTuple_GET_SIZE(mc->args);
1856+
PyObject *newargs = PyTuple_New(1 + callargcount);
18641857
if (newargs == NULL)
18651858
return NULL;
18661859
PyTuple_SET_ITEM(newargs, 0, Py_NewRef(mc->name));
1867-
for (i = 1; i < newarg_size; ++i) {
1868-
PyObject *arg = PyTuple_GET_ITEM(mc->xargs, i);
1869-
PyTuple_SET_ITEM(newargs, i, Py_NewRef(arg));
1860+
for (i = 0; i < callargcount; ++i) {
1861+
PyObject *arg = PyTuple_GET_ITEM(mc->args, i);
1862+
PyTuple_SET_ITEM(newargs, i + 1, Py_NewRef(arg));
18701863
}
18711864
return Py_BuildValue("ON", Py_TYPE(mc), newargs);
18721865
}
@@ -1884,12 +1877,7 @@ methodcaller_reduce(methodcallerobject *mc, PyObject *Py_UNUSED(ignored))
18841877
constructor = PyObject_VectorcallDict(partial, newargs, 2, mc->kwds);
18851878

18861879
Py_DECREF(partial);
1887-
PyObject *args = PyTuple_GetSlice(mc->xargs, 1, PyTuple_GET_SIZE(mc->xargs));
1888-
if (!args) {
1889-
Py_DECREF(constructor);
1890-
return NULL;
1891-
}
1892-
return Py_BuildValue("NO", constructor, args);
1880+
return Py_BuildValue("NO", constructor, mc->args);
18931881
}
18941882
}
18951883

0 commit comments

Comments
 (0)