Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1a9a9d5

Browse files
committed
Issue #1868: Eliminate subtle timing issues in thread-local objects by
getting rid of the cached copy of thread-local attribute dictionary.
1 parent 64a38c0 commit 1a9a9d5

6 files changed

Lines changed: 183 additions & 92 deletions

File tree

Include/object.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,14 @@ PyAPI_FUNC(int) PyCallable_Check(PyObject *);
448448

449449
PyAPI_FUNC(void) PyObject_ClearWeakRefs(PyObject *);
450450

451+
/* Same as PyObject_Generic{Get,Set}Attr, but passing the attributes
452+
dict as the last parameter. */
453+
PyAPI_FUNC(PyObject *)
454+
_PyObject_GenericGetAttrWithDict(PyObject *, PyObject *, PyObject *);
455+
PyAPI_FUNC(int)
456+
_PyObject_GenericSetAttrWithDict(PyObject *, PyObject *,
457+
PyObject *, PyObject *);
458+
451459

452460
/* PyObject_Dir(obj) acts like Python builtins.dir(obj), returning a
453461
list of strings. PyObject_Dir(NULL) is like builtins.dir(),

Lib/_threading_local.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ def __getattribute__(self, name):
194194
lock.release()
195195

196196
def __setattr__(self, name, value):
197+
if name == '__dict__':
198+
raise AttributeError(
199+
"%r object attribute '__dict__' is read-only"
200+
% self.__class__.__name__)
197201
lock = object.__getattribute__(self, '_local__lock')
198202
lock.acquire()
199203
try:
@@ -203,6 +207,10 @@ def __setattr__(self, name, value):
203207
lock.release()
204208

205209
def __delattr__(self, name):
210+
if name == '__dict__':
211+
raise AttributeError(
212+
"%r object attribute '__dict__' is read-only"
213+
% self.__class__.__name__)
206214
lock = object.__getattribute__(self, '_local__lock')
207215
lock.acquire()
208216
try:

Lib/test/test_threading_local.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,67 @@ def __init__(self, *args, **kwargs):
123123
self.assertRaises(TypeError, self._local, a=1)
124124
self.assertRaises(TypeError, self._local, 1)
125125

126+
def _test_one_class(self, c):
127+
self._failed = "No error message set or cleared."
128+
obj = c()
129+
e1 = threading.Event()
130+
e2 = threading.Event()
131+
132+
def f1():
133+
obj.x = 'foo'
134+
obj.y = 'bar'
135+
del obj.y
136+
e1.set()
137+
e2.wait()
138+
139+
def f2():
140+
try:
141+
foo = obj.x
142+
except AttributeError:
143+
# This is expected -- we haven't set obj.x in this thread yet!
144+
self._failed = "" # passed
145+
else:
146+
self._failed = ('Incorrectly got value %r from class %r\n' %
147+
(foo, c))
148+
sys.stderr.write(self._failed)
149+
150+
t1 = threading.Thread(target=f1)
151+
t1.start()
152+
e1.wait()
153+
t2 = threading.Thread(target=f2)
154+
t2.start()
155+
t2.join()
156+
# The test is done; just let t1 know it can exit, and wait for it.
157+
e2.set()
158+
t1.join()
159+
160+
self.assertFalse(self._failed, self._failed)
161+
162+
def test_threading_local(self):
163+
self._test_one_class(self._local)
164+
165+
def test_threading_local_subclass(self):
166+
class LocalSubclass(self._local):
167+
"""To test that subclasses behave properly."""
168+
self._test_one_class(LocalSubclass)
169+
170+
def _test_dict_attribute(self, cls):
171+
obj = cls()
172+
obj.x = 5
173+
self.assertEqual(obj.__dict__, {'x': 5})
174+
with self.assertRaises(AttributeError):
175+
obj.__dict__ = {}
176+
with self.assertRaises(AttributeError):
177+
del obj.__dict__
178+
179+
def test_dict_attribute(self):
180+
self._test_dict_attribute(self._local)
181+
182+
def test_dict_attribute_subclass(self):
183+
class LocalSubclass(self._local):
184+
"""To test that subclasses behave properly."""
185+
self._test_dict_attribute(LocalSubclass)
186+
126187

127188
class ThreadLocalTest(unittest.TestCase, BaseLocalTest):
128189
_local = _thread._local
@@ -140,7 +201,6 @@ class X:
140201
gc.collect()
141202
self.assertIs(wr(), None)
142203

143-
144204
class PyThreadingLocalTest(unittest.TestCase, BaseLocalTest):
145205
_local = _threading_local.local
146206

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ Extensions
132132
Library
133133
-------
134134

135+
- Issue #1868: Eliminate subtle timing issues in thread-local objects by
136+
getting rid of the cached copy of thread-local attribute dictionary.
137+
135138
- Issue #1512791: In setframerate() in the wave module, non-integral
136139
frame rates are rounded to the nearest integer.
137140

Modules/_threadmodule.c

Lines changed: 38 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
static PyObject *ThreadError;
1717
static long nb_threads = 0;
18+
static PyObject *str_dict;
1819

1920
/* Lock objects */
2021

@@ -586,8 +587,6 @@ typedef struct {
586587
PyObject *key;
587588
PyObject *args;
588589
PyObject *kw;
589-
/* The current thread's local dict (necessary for tp_dictoffset) */
590-
PyObject *dict;
591590
PyObject *weakreflist; /* List of weak references to self */
592591
/* A {localdummy weakref -> localdict} dict */
593592
PyObject *dummies;
@@ -599,9 +598,9 @@ typedef struct {
599598
static PyObject *_ldict(localobject *self);
600599
static PyObject *_localdummy_destroyed(PyObject *meth_self, PyObject *dummyweakref);
601600

602-
/* Create and register the dummy for the current thread, as well as the
603-
corresponding local dict */
604-
static int
601+
/* Create and register the dummy for the current thread.
602+
Returns a borrowed reference of the corresponding local dict */
603+
static PyObject *
605604
_local_create_dummy(localobject *self)
606605
{
607606
PyObject *tdict, *ldict = NULL, *wr = NULL;
@@ -637,15 +636,14 @@ _local_create_dummy(localobject *self)
637636
goto err;
638637
Py_CLEAR(dummy);
639638

640-
Py_CLEAR(self->dict);
641-
self->dict = ldict;
642-
return 0;
639+
Py_DECREF(ldict);
640+
return ldict;
643641

644642
err:
645643
Py_XDECREF(ldict);
646644
Py_XDECREF(wr);
647645
Py_XDECREF(dummy);
648-
return -1;
646+
return NULL;
649647
}
650648

651649
static PyObject *
@@ -691,7 +689,7 @@ local_new(PyTypeObject *type, PyObject *args, PyObject *kw)
691689
if (self->wr_callback == NULL)
692690
goto err;
693691

694-
if (_local_create_dummy(self) < 0)
692+
if (_local_create_dummy(self) == NULL)
695693
goto err;
696694

697695
return (PyObject *)self;
@@ -707,7 +705,6 @@ local_traverse(localobject *self, visitproc visit, void *arg)
707705
Py_VISIT(self->args);
708706
Py_VISIT(self->kw);
709707
Py_VISIT(self->dummies);
710-
Py_VISIT(self->dict);
711708
return 0;
712709
}
713710

@@ -718,7 +715,6 @@ local_clear(localobject *self)
718715
Py_CLEAR(self->args);
719716
Py_CLEAR(self->kw);
720717
Py_CLEAR(self->dummies);
721-
Py_CLEAR(self->dict);
722718
Py_CLEAR(self->wr_callback);
723719
/* Remove all strong references to dummies from the thread states */
724720
if (self->key
@@ -764,9 +760,9 @@ _ldict(localobject *self)
764760

765761
dummy = PyDict_GetItem(tdict, self->key);
766762
if (dummy == NULL) {
767-
if (_local_create_dummy(self) < 0)
763+
ldict = _local_create_dummy(self);
764+
if (ldict == NULL)
768765
return NULL;
769-
ldict = self->dict;
770766

771767
if (Py_TYPE(self)->tp_init != PyBaseObject_Type.tp_init &&
772768
Py_TYPE(self)->tp_init((PyObject*)self,
@@ -783,44 +779,32 @@ _ldict(localobject *self)
783779
ldict = ((localdummyobject *) dummy)->localdict;
784780
}
785781

786-
/* The call to tp_init above may have caused another thread to run.
787-
Install our ldict again. */
788-
if (self->dict != ldict) {
789-
Py_INCREF(ldict);
790-
Py_CLEAR(self->dict);
791-
self->dict = ldict;
792-
}
793-
794782
return ldict;
795783
}
796784

797785
static int
798786
local_setattro(localobject *self, PyObject *name, PyObject *v)
799787
{
800788
PyObject *ldict;
789+
int r;
801790

802791
ldict = _ldict(self);
803792
if (ldict == NULL)
804793
return -1;
805794

806-
return PyObject_GenericSetAttr((PyObject *)self, name, v);
807-
}
795+
r = PyObject_RichCompareBool(name, str_dict, Py_EQ);
796+
if (r == 1) {
797+
PyErr_Format(PyExc_AttributeError,
798+
"'%.50s' object attribute '%U' is read-only",
799+
Py_TYPE(self)->tp_name, name);
800+
return -1;
801+
}
802+
if (r == -1)
803+
return -1;
808804

809-
static PyObject *
810-
local_getdict(localobject *self, void *closure)
811-
{
812-
PyObject *ldict;
813-
ldict = _ldict(self);
814-
Py_XINCREF(ldict);
815-
return ldict;
805+
return _PyObject_GenericSetAttrWithDict((PyObject *)self, name, v, ldict);
816806
}
817807

818-
static PyGetSetDef local_getset[] = {
819-
{"__dict__", (getter)local_getdict, (setter)NULL,
820-
"Local-data dictionary", NULL},
821-
{NULL} /* Sentinel */
822-
};
823-
824808
static PyObject *local_getattro(localobject *, PyObject *);
825809

826810
static PyTypeObject localtype = {
@@ -854,12 +838,12 @@ static PyTypeObject localtype = {
854838
/* tp_iternext */ 0,
855839
/* tp_methods */ 0,
856840
/* tp_members */ 0,
857-
/* tp_getset */ local_getset,
841+
/* tp_getset */ 0,
858842
/* tp_base */ 0,
859843
/* tp_dict */ 0, /* internal use */
860844
/* tp_descr_get */ 0,
861845
/* tp_descr_set */ 0,
862-
/* tp_dictoffset */ offsetof(localobject, dict),
846+
/* tp_dictoffset */ 0,
863847
/* tp_init */ 0,
864848
/* tp_alloc */ 0,
865849
/* tp_new */ local_new,
@@ -871,20 +855,29 @@ static PyObject *
871855
local_getattro(localobject *self, PyObject *name)
872856
{
873857
PyObject *ldict, *value;
858+
int r;
874859

875860
ldict = _ldict(self);
876861
if (ldict == NULL)
877862
return NULL;
878863

864+
r = PyObject_RichCompareBool(name, str_dict, Py_EQ);
865+
if (r == 1) {
866+
Py_INCREF(ldict);
867+
return ldict;
868+
}
869+
if (r == -1)
870+
return NULL;
871+
879872
if (Py_TYPE(self) != &localtype)
880873
/* use generic lookup for subtypes */
881-
return PyObject_GenericGetAttr((PyObject *)self, name);
874+
return _PyObject_GenericGetAttrWithDict((PyObject *)self, name, ldict);
882875

883876
/* Optimization: just look in dict ourselves */
884877
value = PyDict_GetItem(ldict, name);
885878
if (value == NULL)
886879
/* Fall back on generic to get __class__ and __dict__ */
887-
return PyObject_GenericGetAttr((PyObject *)self, name);
880+
return _PyObject_GenericGetAttrWithDict((PyObject *)self, name, ldict);
888881

889882
Py_INCREF(value);
890883
return value;
@@ -909,8 +902,6 @@ _localdummy_destroyed(PyObject *localweakref, PyObject *dummyweakref)
909902
PyObject *ldict;
910903
ldict = PyDict_GetItem(self->dummies, dummyweakref);
911904
if (ldict != NULL) {
912-
if (ldict == self->dict)
913-
Py_CLEAR(self->dict);
914905
PyDict_DelItem(self->dummies, dummyweakref);
915906
}
916907
if (PyErr_Occurred())
@@ -1278,6 +1269,10 @@ PyInit__thread(void)
12781269

12791270
nb_threads = 0;
12801271

1272+
str_dict = PyUnicode_InternFromString("__dict__");
1273+
if (str_dict == NULL)
1274+
return NULL;
1275+
12811276
/* Initialize the C thread library */
12821277
PyThread_init_thread();
12831278
return m;

0 commit comments

Comments
 (0)