summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorPieter Eendebak <pieter.eendebak@gmail.com>2024-12-11 15:06:07 (GMT)
committerGitHub <noreply@github.com>2024-12-11 15:06:07 (GMT)
commitb0f278ff0551b06191cec01445c577e3b25570da (patch)
tree4a9117ecb9fbb404229a30536a3ae87ac68f4b2f /Lib
parent5a23994a3dbee43a0b08f5920032f60f38b63071 (diff)
downloadcpython-b0f278ff0551b06191cec01445c577e3b25570da.zip
cpython-b0f278ff0551b06191cec01445c577e3b25570da.tar.gz
cpython-b0f278ff0551b06191cec01445c577e3b25570da.tar.bz2
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.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/test_free_threading/test_methodcaller.py33
-rw-r--r--Lib/test/test_operator.py13
2 files changed, 46 insertions, 0 deletions
diff --git a/Lib/test/test_free_threading/test_methodcaller.py b/Lib/test/test_free_threading/test_methodcaller.py
new file mode 100644
index 0000000..8846b00
--- /dev/null
+++ b/Lib/test/test_free_threading/test_methodcaller.py
@@ -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()
diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py
index 812d464..82578a0 100644
--- a/Lib/test/test_operator.py
+++ b/Lib/test/test_operator.py
@@ -482,6 +482,8 @@ class OperatorTestCase:
return f
def baz(*args, **kwds):
return kwds['name'], kwds['self']
+ def return_arguments(self, *args, **kwds):
+ return args, kwds
a = A()
f = operator.methodcaller('foo')
self.assertRaises(IndexError, f, a)
@@ -498,6 +500,17 @@ class OperatorTestCase:
f = operator.methodcaller('baz', name='spam', self='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):
operator = self.module
class C(object):