Skip to content

Commit a3b6577

Browse files
[3.10] GH-100892: Fix race in clearing threading.local (GH-100922). (#100938)
(cherry picked from commit 762745a) Co-authored-by: Kumar Aditya <[email protected]>
1 parent 5aa8b9e commit a3b6577

File tree

4 files changed

+70
-15
lines changed

4 files changed

+70
-15
lines changed

Lib/test/test_threading_local.py

+13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from doctest import DocTestSuite
44
from test import support
55
from test.support import threading_helper
6+
from test.support.import_helper import import_module
67
import weakref
78
import gc
89

@@ -194,6 +195,18 @@ class X:
194195
self.assertIsNone(wr())
195196

196197

198+
def test_threading_local_clear_race(self):
199+
# See https://github.com/python/cpython/issues/100892
200+
201+
_testcapi = import_module('_testcapi')
202+
_testcapi.call_in_temporary_c_thread(lambda: None, False)
203+
204+
for _ in range(1000):
205+
_ = threading.local()
206+
207+
_testcapi.join_temporary_c_thread()
208+
209+
197210
class ThreadLocalTest(unittest.TestCase, BaseLocalTest):
198211
_local = _thread._local
199212

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix race while iterating over thread states in clearing :class:`threading.local`. Patch by Kumar Aditya.

Modules/_testcapimodule.c

+36-5
Original file line numberDiff line numberDiff line change
@@ -4368,12 +4368,19 @@ temporary_c_thread(void *data)
43684368
PyThread_exit_thread();
43694369
}
43704370

4371+
static test_c_thread_t test_c_thread;
4372+
43714373
static PyObject *
4372-
call_in_temporary_c_thread(PyObject *self, PyObject *callback)
4374+
call_in_temporary_c_thread(PyObject *self, PyObject *args)
43734375
{
43744376
PyObject *res = NULL;
4375-
test_c_thread_t test_c_thread;
4377+
PyObject *callback = NULL;
43764378
long thread;
4379+
int wait = 1;
4380+
if (!PyArg_ParseTuple(args, "O|i", &callback, &wait))
4381+
{
4382+
return NULL;
4383+
}
43774384

43784385
test_c_thread.start_event = PyThread_allocate_lock();
43794386
test_c_thread.exit_event = PyThread_allocate_lock();
@@ -4400,6 +4407,10 @@ call_in_temporary_c_thread(PyObject *self, PyObject *callback)
44004407
PyThread_acquire_lock(test_c_thread.start_event, 1);
44014408
PyThread_release_lock(test_c_thread.start_event);
44024409

4410+
if (!wait) {
4411+
Py_RETURN_NONE;
4412+
}
4413+
44034414
Py_BEGIN_ALLOW_THREADS
44044415
PyThread_acquire_lock(test_c_thread.exit_event, 1);
44054416
PyThread_release_lock(test_c_thread.exit_event);
@@ -4410,13 +4421,32 @@ call_in_temporary_c_thread(PyObject *self, PyObject *callback)
44104421

44114422
exit:
44124423
Py_CLEAR(test_c_thread.callback);
4413-
if (test_c_thread.start_event)
4424+
if (test_c_thread.start_event) {
44144425
PyThread_free_lock(test_c_thread.start_event);
4415-
if (test_c_thread.exit_event)
4426+
test_c_thread.start_event = NULL;
4427+
}
4428+
if (test_c_thread.exit_event) {
44164429
PyThread_free_lock(test_c_thread.exit_event);
4430+
test_c_thread.exit_event = NULL;
4431+
}
44174432
return res;
44184433
}
44194434

4435+
static PyObject *
4436+
join_temporary_c_thread(PyObject *self, PyObject *Py_UNUSED(ignored))
4437+
{
4438+
Py_BEGIN_ALLOW_THREADS
4439+
PyThread_acquire_lock(test_c_thread.exit_event, 1);
4440+
PyThread_release_lock(test_c_thread.exit_event);
4441+
Py_END_ALLOW_THREADS
4442+
Py_CLEAR(test_c_thread.callback);
4443+
PyThread_free_lock(test_c_thread.start_event);
4444+
test_c_thread.start_event = NULL;
4445+
PyThread_free_lock(test_c_thread.exit_event);
4446+
test_c_thread.exit_event = NULL;
4447+
Py_RETURN_NONE;
4448+
}
4449+
44204450
/* marshal */
44214451

44224452
static PyObject*
@@ -5878,8 +5908,9 @@ static PyMethodDef TestMethods[] = {
58785908
{"docstring_with_signature_with_defaults",
58795909
(PyCFunction)test_with_docstring, METH_NOARGS,
58805910
docstring_with_signature_with_defaults},
5881-
{"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_O,
5911+
{"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_VARARGS,
58825912
PyDoc_STR("set_error_class(error_class) -> None")},
5913+
{"join_temporary_c_thread", join_temporary_c_thread, METH_NOARGS},
58835914
{"pymarshal_write_long_to_file",
58845915
pymarshal_write_long_to_file, METH_VARARGS},
58855916
{"pymarshal_write_object_to_file",

Modules/_threadmodule.c

+20-10
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,11 @@ local_traverse(localobject *self, visitproc visit, void *arg)
842842
return 0;
843843
}
844844

845+
#define HEAD_LOCK(runtime) \
846+
PyThread_acquire_lock((runtime)->interpreters.mutex, WAIT_LOCK)
847+
#define HEAD_UNLOCK(runtime) \
848+
PyThread_release_lock((runtime)->interpreters.mutex)
849+
845850
static int
846851
local_clear(localobject *self)
847852
{
@@ -852,18 +857,23 @@ local_clear(localobject *self)
852857
/* Remove all strong references to dummies from the thread states */
853858
if (self->key) {
854859
PyInterpreterState *interp = _PyInterpreterState_GET();
860+
_PyRuntimeState *runtime = &_PyRuntime;
861+
HEAD_LOCK(runtime);
855862
PyThreadState *tstate = PyInterpreterState_ThreadHead(interp);
856-
for(; tstate; tstate = PyThreadState_Next(tstate)) {
857-
if (tstate->dict == NULL) {
858-
continue;
859-
}
860-
PyObject *v = _PyDict_Pop(tstate->dict, self->key, Py_None);
861-
if (v != NULL) {
862-
Py_DECREF(v);
863-
}
864-
else {
865-
PyErr_Clear();
863+
HEAD_UNLOCK(runtime);
864+
while (tstate) {
865+
if (tstate->dict) {
866+
PyObject *v = _PyDict_Pop(tstate->dict, self->key, Py_None);
867+
if (v != NULL) {
868+
Py_DECREF(v);
869+
}
870+
else {
871+
PyErr_Clear();
872+
}
866873
}
874+
HEAD_LOCK(runtime);
875+
tstate = PyThreadState_Next(tstate);
876+
HEAD_UNLOCK(runtime);
867877
}
868878
}
869879
return 0;

0 commit comments

Comments
 (0)