diff options
author | Pieter Eendebak <pieter.eendebak@gmail.com> | 2024-12-11 15:06:07 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-11 15:06:07 (GMT) |
commit | b0f278ff0551b06191cec01445c577e3b25570da (patch) | |
tree | 4a9117ecb9fbb404229a30536a3ae87ac68f4b2f /Lib | |
parent | 5a23994a3dbee43a0b08f5920032f60f38b63071 (diff) | |
download | cpython-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.py | 33 | ||||
-rw-r--r-- | Lib/test/test_operator.py | 13 |
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): |