https://github.com/python/cpython/commit/8554c0917e25a7abe12b3000f1589b6566c91a25
commit: 8554c0917e25a7abe12b3000f1589b6566c91a25
branch: main
author: Alper <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2025-09-06T13:19:14+05:30
summary:

gh-116738: make `cProfile` module thread-safe (#138229)

Co-authored-by: Kumar Aditya <[email protected]>

files:
A Lib/test/test_free_threading/test_cprofile.py
A 
Misc/NEWS.d/next/Core_and_Builtins/2025-08-28-09-29-46.gh-issue-116738.yLZJpV.rst
M Modules/_lsprof.c
M Modules/clinic/_lsprof.c.h

diff --git a/Lib/test/test_free_threading/test_cprofile.py 
b/Lib/test/test_free_threading/test_cprofile.py
new file mode 100644
index 00000000000000..361b800d6b9029
--- /dev/null
+++ b/Lib/test/test_free_threading/test_cprofile.py
@@ -0,0 +1,43 @@
+import unittest
+
+from test.support import threading_helper
+
+import cProfile
+import pstats
+
+
+NTHREADS = 10
+INSERT_PER_THREAD = 1000
+
+
+@threading_helper.requires_working_threading()
+class TestCProfile(unittest.TestCase):
+    def test_cprofile_racing_list_insert(self):
+        def list_insert(lst):
+            for i in range(INSERT_PER_THREAD):
+                lst.insert(0, i)
+
+        lst = []
+
+        with cProfile.Profile() as pr:
+            threading_helper.run_concurrently(
+                worker_func=list_insert, nthreads=NTHREADS, args=(lst,)
+            )
+            pr.create_stats()
+            ps = pstats.Stats(pr)
+            stats_profile = ps.get_stats_profile()
+            list_insert_profile = stats_profile.func_profiles[
+                "<method 'insert' of 'list' objects>"
+            ]
+            # Even though there is no explicit recursive call to insert,
+            # cProfile may record some calls as recursive due to limitations
+            # in its handling of multithreaded programs. This issue is not
+            # directly related to FT Python itself; however, it tends to be
+            # more noticeable when using FT Python. Therefore, consider only
+            # the calls section and disregard the recursive part.
+            list_insert_ncalls = list_insert_profile.ncalls.split("/")[0]
+            self.assertEqual(
+                int(list_insert_ncalls), NTHREADS * INSERT_PER_THREAD
+            )
+
+        self.assertEqual(len(lst), NTHREADS * INSERT_PER_THREAD)
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-28-09-29-46.gh-issue-116738.yLZJpV.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-28-09-29-46.gh-issue-116738.yLZJpV.rst
new file mode 100644
index 00000000000000..2bb68035b56509
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-28-09-29-46.gh-issue-116738.yLZJpV.rst
@@ -0,0 +1,2 @@
+Make :mod:`cProfile` thread-safe on the :term:`free threaded <free
+threading>` build.
diff --git a/Modules/_lsprof.c b/Modules/_lsprof.c
index d0074b2a0d1f4d..c20dbc3f4f4bfb 100644
--- a/Modules/_lsprof.c
+++ b/Modules/_lsprof.c
@@ -534,6 +534,7 @@ static int statsForEntry(rotating_node_t *node, void *arg)
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler.getstats
 
     cls: defining_class
@@ -565,7 +566,7 @@ profiler_subentry objects:
 
 static PyObject *
 _lsprof_Profiler_getstats_impl(ProfilerObject *self, PyTypeObject *cls)
-/*[clinic end generated code: output=1806ef720019ee03 input=445e193ef4522902]*/
+/*[clinic end generated code: output=1806ef720019ee03 input=3dc69eb85ed73d91]*/
 {
     statscollector_t collect;
     collect.state = _PyType_GetModuleState(cls);
@@ -613,6 +614,7 @@ setBuiltins(ProfilerObject *pObj, int nvalue)
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler._pystart_callback
 
     code: object
@@ -624,7 +626,7 @@ _lsprof.Profiler._pystart_callback
 static PyObject *
 _lsprof_Profiler__pystart_callback_impl(ProfilerObject *self, PyObject *code,
                                         PyObject *instruction_offset)
-/*[clinic end generated code: output=5fec8b7ad5ed25e8 input=b166e6953c579cda]*/
+/*[clinic end generated code: output=5fec8b7ad5ed25e8 input=b61a0e79cf1f8499]*/
 {
     ptrace_enter_call((PyObject*)self, (void *)code, code);
 
@@ -632,6 +634,7 @@ _lsprof_Profiler__pystart_callback_impl(ProfilerObject 
*self, PyObject *code,
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler._pythrow_callback
 
     code: object
@@ -645,7 +648,7 @@ static PyObject *
 _lsprof_Profiler__pythrow_callback_impl(ProfilerObject *self, PyObject *code,
                                         PyObject *instruction_offset,
                                         PyObject *exception)
-/*[clinic end generated code: output=0a32988919dfb94c input=fd728fc2c074f5e6]*/
+/*[clinic end generated code: output=0a32988919dfb94c input=60c7f272206d3758]*/
 {
     ptrace_enter_call((PyObject*)self, (void *)code, code);
 
@@ -653,6 +656,7 @@ _lsprof_Profiler__pythrow_callback_impl(ProfilerObject 
*self, PyObject *code,
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler._pyreturn_callback
 
     code: object
@@ -667,7 +671,7 @@ _lsprof_Profiler__pyreturn_callback_impl(ProfilerObject 
*self,
                                          PyObject *code,
                                          PyObject *instruction_offset,
                                          PyObject *retval)
-/*[clinic end generated code: output=9e2f6fc1b882c51e input=667ffaeb2fa6fd1f]*/
+/*[clinic end generated code: output=9e2f6fc1b882c51e input=0ddcc1ec53faa928]*/
 {
     ptrace_leave_call((PyObject*)self, (void *)code);
 
@@ -703,6 +707,7 @@ PyObject* get_cfunc_from_callable(PyObject* callable, 
PyObject* self_arg, PyObje
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler._ccall_callback
 
     code: object
@@ -717,7 +722,7 @@ static PyObject *
 _lsprof_Profiler__ccall_callback_impl(ProfilerObject *self, PyObject *code,
                                       PyObject *instruction_offset,
                                       PyObject *callable, PyObject *self_arg)
-/*[clinic end generated code: output=152db83cabd18cad input=0e66687cfb95c001]*/
+/*[clinic end generated code: output=152db83cabd18cad input=2fc1e0630ee5e32b]*/
 {
     if (self->flags & POF_BUILTINS) {
         PyObject* cfunc = get_cfunc_from_callable(callable, self_arg, 
self->missing);
@@ -733,6 +738,7 @@ _lsprof_Profiler__ccall_callback_impl(ProfilerObject *self, 
PyObject *code,
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler._creturn_callback
 
     code: object
@@ -748,7 +754,7 @@ _lsprof_Profiler__creturn_callback_impl(ProfilerObject 
*self, PyObject *code,
                                         PyObject *instruction_offset,
                                         PyObject *callable,
                                         PyObject *self_arg)
-/*[clinic end generated code: output=1e886dde8fed8fb0 input=b18afe023746923a]*/
+/*[clinic end generated code: output=1e886dde8fed8fb0 input=bdc246d6b5b8714a]*/
 {
     if (self->flags & POF_BUILTINS) {
         PyObject* cfunc = get_cfunc_from_callable(callable, self_arg, 
self->missing);
@@ -780,6 +786,7 @@ static const struct {
 
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler.enable
 
     subcalls: bool = True
@@ -796,7 +803,7 @@ Start collecting profiling information.
 static PyObject *
 _lsprof_Profiler_enable_impl(ProfilerObject *self, int subcalls,
                              int builtins)
-/*[clinic end generated code: output=1e747f9dc1edd571 input=9ab81405107ab7f1]*/
+/*[clinic end generated code: output=1e747f9dc1edd571 input=0b88115b1c796173]*/
 {
     int all_events = 0;
     if (setSubcalls(self, subcalls) < 0 || setBuiltins(self, builtins) < 0) {
@@ -869,6 +876,7 @@ flush_unmatched(ProfilerObject *pObj)
 
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler.disable
 
 Stop collecting profiling information.
@@ -876,7 +884,7 @@ Stop collecting profiling information.
 
 static PyObject *
 _lsprof_Profiler_disable_impl(ProfilerObject *self)
-/*[clinic end generated code: output=838cffef7f651870 input=05700b3fc68d1f50]*/
+/*[clinic end generated code: output=838cffef7f651870 input=f7e4787cae20f7f6]*/
 {
     if (self->flags & POF_EXT_TIMER) {
         PyErr_SetString(PyExc_RuntimeError,
@@ -928,6 +936,7 @@ _lsprof_Profiler_disable_impl(ProfilerObject *self)
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler.clear
 
 Clear all profiling information collected so far.
@@ -935,7 +944,7 @@ Clear all profiling information collected so far.
 
 static PyObject *
 _lsprof_Profiler_clear_impl(ProfilerObject *self)
-/*[clinic end generated code: output=dd1c668fb84b1335 input=fbe1f88c28be4f98]*/
+/*[clinic end generated code: output=dd1c668fb84b1335 input=4aab219d5d7a9bec]*/
 {
     if (self->flags & POF_EXT_TIMER) {
         PyErr_SetString(PyExc_RuntimeError,
diff --git a/Modules/clinic/_lsprof.c.h b/Modules/clinic/_lsprof.c.h
index c426cd6fe02f39..acb4aaf27e377f 100644
--- a/Modules/clinic/_lsprof.c.h
+++ b/Modules/clinic/_lsprof.c.h
@@ -6,6 +6,7 @@ preserve
 #  include "pycore_gc.h"          // PyGC_Head
 #  include "pycore_runtime.h"     // _Py_ID()
 #endif
+#include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION()
 #include "pycore_modsupport.h"    // _PyArg_CheckPositional()
 
 PyDoc_STRVAR(_lsprof_Profiler_getstats__doc__,
@@ -45,11 +46,18 @@ _lsprof_Profiler_getstats_impl(ProfilerObject *self, 
PyTypeObject *cls);
 static PyObject *
 _lsprof_Profiler_getstats(PyObject *self, PyTypeObject *cls, PyObject *const 
*args, Py_ssize_t nargs, PyObject *kwnames)
 {
+    PyObject *return_value = NULL;
+
     if (nargs || (kwnames && PyTuple_GET_SIZE(kwnames))) {
         PyErr_SetString(PyExc_TypeError, "getstats() takes no arguments");
-        return NULL;
+        goto exit;
     }
-    return _lsprof_Profiler_getstats_impl((ProfilerObject *)self, cls);
+    Py_BEGIN_CRITICAL_SECTION(self);
+    return_value = _lsprof_Profiler_getstats_impl((ProfilerObject *)self, cls);
+    Py_END_CRITICAL_SECTION();
+
+exit:
+    return return_value;
 }
 
 PyDoc_STRVAR(_lsprof_Profiler__pystart_callback__doc__,
@@ -76,7 +84,9 @@ _lsprof_Profiler__pystart_callback(PyObject *self, PyObject 
*const *args, Py_ssi
     }
     code = args[0];
     instruction_offset = args[1];
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler__pystart_callback_impl((ProfilerObject 
*)self, code, instruction_offset);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -109,7 +119,9 @@ _lsprof_Profiler__pythrow_callback(PyObject *self, PyObject 
*const *args, Py_ssi
     code = args[0];
     instruction_offset = args[1];
     exception = args[2];
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler__pythrow_callback_impl((ProfilerObject 
*)self, code, instruction_offset, exception);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -143,7 +155,9 @@ _lsprof_Profiler__pyreturn_callback(PyObject *self, 
PyObject *const *args, Py_ss
     code = args[0];
     instruction_offset = args[1];
     retval = args[2];
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler__pyreturn_callback_impl((ProfilerObject 
*)self, code, instruction_offset, retval);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -178,7 +192,9 @@ _lsprof_Profiler__ccall_callback(PyObject *self, PyObject 
*const *args, Py_ssize
     instruction_offset = args[1];
     callable = args[2];
     self_arg = args[3];
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler__ccall_callback_impl((ProfilerObject 
*)self, code, instruction_offset, callable, self_arg);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -215,7 +231,9 @@ _lsprof_Profiler__creturn_callback(PyObject *self, PyObject 
*const *args, Py_ssi
     instruction_offset = args[1];
     callable = args[2];
     self_arg = args[3];
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler__creturn_callback_impl((ProfilerObject 
*)self, code, instruction_offset, callable, self_arg);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -299,7 +317,9 @@ _lsprof_Profiler_enable(PyObject *self, PyObject *const 
*args, Py_ssize_t nargs,
         goto exit;
     }
 skip_optional_pos:
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler_enable_impl((ProfilerObject *)self, 
subcalls, builtins);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -320,7 +340,13 @@ _lsprof_Profiler_disable_impl(ProfilerObject *self);
 static PyObject *
 _lsprof_Profiler_disable(PyObject *self, PyObject *Py_UNUSED(ignored))
 {
-    return _lsprof_Profiler_disable_impl((ProfilerObject *)self);
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(self);
+    return_value = _lsprof_Profiler_disable_impl((ProfilerObject *)self);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
 }
 
 PyDoc_STRVAR(_lsprof_Profiler_clear__doc__,
@@ -338,7 +364,13 @@ _lsprof_Profiler_clear_impl(ProfilerObject *self);
 static PyObject *
 _lsprof_Profiler_clear(PyObject *self, PyObject *Py_UNUSED(ignored))
 {
-    return _lsprof_Profiler_clear_impl((ProfilerObject *)self);
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(self);
+    return_value = _lsprof_Profiler_clear_impl((ProfilerObject *)self);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
 }
 
 PyDoc_STRVAR(profiler_init__doc__,
@@ -444,4 +476,4 @@ profiler_init(PyObject *self, PyObject *args, PyObject 
*kwargs)
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=9e46985561166c37 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=af26a0b0ddcc3351 input=a9049054013a1b77]*/

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to