Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Lib/test/test_free_threading/test_func_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

from test.support import threading_helper, Py_GIL_DISABLED

try:
import _testcapi
except ImportError:
_testcapi = None

threading_helper.requires_working_threading(module=True)


Expand All @@ -19,6 +24,11 @@ def get_func_annotation_dunder(f, b):
return f.__annotations__


def get_func_annotation_capi(f, b):
b.wait()
return _testcapi.function_get_annotations(f)


def set_func_annotation(f, b):
b.wait()
f.__annotations__ = {'x': int, 'y': int, 'return': int}
Expand Down Expand Up @@ -65,3 +75,24 @@ def bar(x: int, y: float) -> float:

# func_get_annotations returns in-place dict, so bar.__annotations__ should be modified as well
self.assertEqual(bar.__annotations__, {'x': int, 'y': int, 'return': int})

@unittest.skipIf(_testcapi is None, "requires _testcapi")
def test_concurrent_capi_read_and_write(self):
# PyFunction_GetAnnotations() lazily computes and stores the
# annotations dict, so it must hold a critical section on the
# function object, like the __annotations__ getter and setter do.
for _ in range(10):
def foo(x: int) -> int:
return x + 1

with concurrent.futures.ThreadPoolExecutor(max_workers=self.NUM_THREADS) as executor:
b = Barrier(self.NUM_THREADS)
futures = {}
for i in range(self.NUM_THREADS):
func = get_func_annotation_capi if i % 2 else set_func_annotation
futures[executor.submit(func, foo, b)] = i
for fut in concurrent.futures.as_completed(futures):
annotations = fut.result()
# foo always has annotations, either computed from
# foo.__annotate__ or set by set_func_annotation()
self.assertIsInstance(annotations, dict)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a data race between :c:func:`PyFunction_GetAnnotations` and assigning to ``__annotations__`` in the free-threaded build.
7 changes: 6 additions & 1 deletion Objects/funcobject.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* Function object implementation */

#include "Python.h"
#include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION()
#include "pycore_code.h" // _PyCode_VerifyStateless()
#include "pycore_dict.h" // _Py_INCREF_DICT()
#include "pycore_function.h" // _PyFunction_Vectorcall
Expand Down Expand Up @@ -589,7 +590,11 @@ PyFunction_GetAnnotations(PyObject *op)
PyErr_BadInternalCall();
return NULL;
}
return func_get_annotation_dict((PyFunctionObject *)op);
PyObject *annotations;
Py_BEGIN_CRITICAL_SECTION(op);
annotations = func_get_annotation_dict((PyFunctionObject *)op);
Py_END_CRITICAL_SECTION();
return annotations;
}

int
Expand Down
Loading