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
30 changes: 29 additions & 1 deletion Lib/test/test_free_threading/test_collections.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import unittest
from collections import deque
from collections import deque, defaultdict
from copy import copy
from test.support import threading_helper

Expand Down Expand Up @@ -49,5 +49,33 @@ def mutate():
)


class TestDefaultDict(unittest.TestCase):
def test_missing_race(self):
dd = defaultdict(int)

def getter():
for i in range(1000):
dd[f"{i}"]

def setter():
for _ in range(1000):
dd.default_factory = lambda: 0

threading_helper.run_concurrently([getter, setter])

def test_repr_race(self):
dd = defaultdict(int)

def reprer():
for _ in range(1000):
repr(dd)

def setter():
for _ in range(1000):
dd.default_factory = lambda: 0

threading_helper.run_concurrently([reprer, setter])


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fixed data-race in :term:`free-threaded build` in
:class:`collections.defaultdict` when calling :func:`repr` and when adding a
new entry to :class:`~collections.defaultdict` that calls
:attr:`defaultdict.default_factory
<collections.defaultdict.default_factory>`.
53 changes: 37 additions & 16 deletions Modules/_collectionsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ find_module_state_by_def(PyTypeObject *type)
module _collections
class _tuplegetter "_tuplegetterobject *" "clinic_state()->tuplegetter_type"
class _collections.deque "dequeobject *" "clinic_state()->deque_type"
class _collections.defaultdict "defdictobject *" "clinic_state()->defdict_type"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=a033cc2a8476b3f1]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=9c364d64d6b7ca9a]*/

typedef struct dequeobject dequeobject;
typedef struct defdictobject defdictobject;

/* We can safely assume type to be the defining class,
* since tuplegetter is not a base type */
Expand Down Expand Up @@ -2214,27 +2216,36 @@ static PyType_Spec dequereviter_spec = {

/* defaultdict type *********************************************************/

typedef struct {
struct defdictobject {
PyDictObject dict;
PyObject *default_factory;
} defdictobject;
};

#define defdictobject_CAST(op) ((defdictobject *)(op))

static PyType_Spec defdict_spec;

PyDoc_STRVAR(defdict_missing_doc,
"__missing__(key) # Called by __getitem__ for missing key; pseudo-code:\n\
if self.default_factory is None: raise KeyError((key,))\n\
self[key] = value = self.default_factory()\n\
return value\n\
");
/*[clinic input]
@critical_section
_collections.defaultdict.__missing__ as defdict_missing

key: object
/

Called by __getitem__ for missing key.

Pseudo-code:
if self.default_factory is None:
raise KeyError((key,))
self[key] = value = self.default_factory()
return value
[clinic start generated code]*/

static PyObject *
defdict_missing(PyObject *op, PyObject *key)
defdict_missing_impl(defdictobject *self, PyObject *key)
/*[clinic end generated code: output=a21796ca845265e6 input=cd46877f8d688f3e]*/
{
defdictobject *dd = defdictobject_CAST(op);
PyObject *factory = dd->default_factory;
PyObject *factory = self->default_factory;
PyObject *value;
if (factory == NULL || factory == Py_None) {
/* XXX Call dict.__missing__(key) */
Expand All @@ -2249,7 +2260,7 @@ defdict_missing(PyObject *op, PyObject *key)
if (value == NULL)
return value;
PyObject *result = NULL;
(void)PyDict_SetDefaultRef(op, key, value, &result);
(void)PyDict_SetDefaultRef((PyObject *)self, key, value, &result);
// 'result' is NULL, or a strong reference to 'value' or 'op[key]'
Py_DECREF(value);
return result;
Expand Down Expand Up @@ -2338,8 +2349,7 @@ of the dictionary's keys and values");


static PyMethodDef defdict_methods[] = {
{"__missing__", defdict_missing, METH_O,
defdict_missing_doc},
DEFDICT_MISSING_METHODDEF
{"copy", defdict_copy, METH_NOARGS,
defdict_copy_doc},
{"__copy__", defdict_copy, METH_NOARGS,
Expand Down Expand Up @@ -2371,8 +2381,9 @@ defdict_dealloc(PyObject *op)
}

static PyObject *
defdict_repr(PyObject *op)
defdict_repr_lock_held(PyObject *op)
{
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op);
defdictobject *dd = defdictobject_CAST(op);
PyObject *baserepr;
PyObject *defrepr;
Expand Down Expand Up @@ -2409,6 +2420,16 @@ defdict_repr(PyObject *op)
return result;
}

static PyObject *
defdict_repr(PyObject *op)
{
PyObject *return_value;
Py_BEGIN_CRITICAL_SECTION(op);
return_value = defdict_repr_lock_held(op);
Py_END_CRITICAL_SECTION();
return return_value;
}

static PyObject*
defdict_or(PyObject* left, PyObject* right)
{
Expand Down
32 changes: 31 additions & 1 deletion Modules/clinic/_collectionsmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading