Commit 1a9a9d54 authored by Antoine Pitrou's avatar Antoine Pitrou

Issue #1868: Eliminate subtle timing issues in thread-local objects by

getting rid of the cached copy of thread-local attribute dictionary.
parent 64a38c0e
...@@ -448,6 +448,14 @@ PyAPI_FUNC(int) PyCallable_Check(PyObject *); ...@@ -448,6 +448,14 @@ PyAPI_FUNC(int) PyCallable_Check(PyObject *);
PyAPI_FUNC(void) PyObject_ClearWeakRefs(PyObject *); PyAPI_FUNC(void) PyObject_ClearWeakRefs(PyObject *);
/* Same as PyObject_Generic{Get,Set}Attr, but passing the attributes
dict as the last parameter. */
PyAPI_FUNC(PyObject *)
_PyObject_GenericGetAttrWithDict(PyObject *, PyObject *, PyObject *);
PyAPI_FUNC(int)
_PyObject_GenericSetAttrWithDict(PyObject *, PyObject *,
PyObject *, PyObject *);
/* PyObject_Dir(obj) acts like Python builtins.dir(obj), returning a /* PyObject_Dir(obj) acts like Python builtins.dir(obj), returning a
list of strings. PyObject_Dir(NULL) is like builtins.dir(), list of strings. PyObject_Dir(NULL) is like builtins.dir(),
......
...@@ -194,6 +194,10 @@ class local(_localbase): ...@@ -194,6 +194,10 @@ class local(_localbase):
lock.release() lock.release()
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name == '__dict__':
raise AttributeError(
"%r object attribute '__dict__' is read-only"
% self.__class__.__name__)
lock = object.__getattribute__(self, '_local__lock') lock = object.__getattribute__(self, '_local__lock')
lock.acquire() lock.acquire()
try: try:
...@@ -203,6 +207,10 @@ class local(_localbase): ...@@ -203,6 +207,10 @@ class local(_localbase):
lock.release() lock.release()
def __delattr__(self, name): def __delattr__(self, name):
if name == '__dict__':
raise AttributeError(
"%r object attribute '__dict__' is read-only"
% self.__class__.__name__)
lock = object.__getattribute__(self, '_local__lock') lock = object.__getattribute__(self, '_local__lock')
lock.acquire() lock.acquire()
try: try:
......
...@@ -123,6 +123,67 @@ class BaseLocalTest: ...@@ -123,6 +123,67 @@ class BaseLocalTest:
self.assertRaises(TypeError, self._local, a=1) self.assertRaises(TypeError, self._local, a=1)
self.assertRaises(TypeError, self._local, 1) self.assertRaises(TypeError, self._local, 1)
def _test_one_class(self, c):
self._failed = "No error message set or cleared."
obj = c()
e1 = threading.Event()
e2 = threading.Event()
def f1():
obj.x = 'foo'
obj.y = 'bar'
del obj.y
e1.set()
e2.wait()
def f2():
try:
foo = obj.x
except AttributeError:
# This is expected -- we haven't set obj.x in this thread yet!
self._failed = "" # passed
else:
self._failed = ('Incorrectly got value %r from class %r\n' %
(foo, c))
sys.stderr.write(self._failed)
t1 = threading.Thread(target=f1)
t1.start()
e1.wait()
t2 = threading.Thread(target=f2)
t2.start()
t2.join()
# The test is done; just let t1 know it can exit, and wait for it.
e2.set()
t1.join()
self.assertFalse(self._failed, self._failed)
def test_threading_local(self):
self._test_one_class(self._local)
def test_threading_local_subclass(self):
class LocalSubclass(self._local):
"""To test that subclasses behave properly."""
self._test_one_class(LocalSubclass)
def _test_dict_attribute(self, cls):
obj = cls()
obj.x = 5
self.assertEqual(obj.__dict__, {'x': 5})
with self.assertRaises(AttributeError):
obj.__dict__ = {}
with self.assertRaises(AttributeError):
del obj.__dict__
def test_dict_attribute(self):
self._test_dict_attribute(self._local)
def test_dict_attribute_subclass(self):
class LocalSubclass(self._local):
"""To test that subclasses behave properly."""
self._test_dict_attribute(LocalSubclass)
class ThreadLocalTest(unittest.TestCase, BaseLocalTest): class ThreadLocalTest(unittest.TestCase, BaseLocalTest):
_local = _thread._local _local = _thread._local
...@@ -140,7 +201,6 @@ class ThreadLocalTest(unittest.TestCase, BaseLocalTest): ...@@ -140,7 +201,6 @@ class ThreadLocalTest(unittest.TestCase, BaseLocalTest):
gc.collect() gc.collect()
self.assertIs(wr(), None) self.assertIs(wr(), None)
class PyThreadingLocalTest(unittest.TestCase, BaseLocalTest): class PyThreadingLocalTest(unittest.TestCase, BaseLocalTest):
_local = _threading_local.local _local = _threading_local.local
......
...@@ -132,6 +132,9 @@ Extensions ...@@ -132,6 +132,9 @@ Extensions
Library Library
------- -------
- Issue #1868: Eliminate subtle timing issues in thread-local objects by
getting rid of the cached copy of thread-local attribute dictionary.
- Issue #1512791: In setframerate() in the wave module, non-integral - Issue #1512791: In setframerate() in the wave module, non-integral
frame rates are rounded to the nearest integer. frame rates are rounded to the nearest integer.
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
static PyObject *ThreadError; static PyObject *ThreadError;
static long nb_threads = 0; static long nb_threads = 0;
static PyObject *str_dict;
/* Lock objects */ /* Lock objects */
...@@ -586,8 +587,6 @@ typedef struct { ...@@ -586,8 +587,6 @@ typedef struct {
PyObject *key; PyObject *key;
PyObject *args; PyObject *args;
PyObject *kw; PyObject *kw;
/* The current thread's local dict (necessary for tp_dictoffset) */
PyObject *dict;
PyObject *weakreflist; /* List of weak references to self */ PyObject *weakreflist; /* List of weak references to self */
/* A {localdummy weakref -> localdict} dict */ /* A {localdummy weakref -> localdict} dict */
PyObject *dummies; PyObject *dummies;
...@@ -599,9 +598,9 @@ typedef struct { ...@@ -599,9 +598,9 @@ typedef struct {
static PyObject *_ldict(localobject *self); static PyObject *_ldict(localobject *self);
static PyObject *_localdummy_destroyed(PyObject *meth_self, PyObject *dummyweakref); static PyObject *_localdummy_destroyed(PyObject *meth_self, PyObject *dummyweakref);
/* Create and register the dummy for the current thread, as well as the /* Create and register the dummy for the current thread.
corresponding local dict */ Returns a borrowed reference of the corresponding local dict */
static int static PyObject *
_local_create_dummy(localobject *self) _local_create_dummy(localobject *self)
{ {
PyObject *tdict, *ldict = NULL, *wr = NULL; PyObject *tdict, *ldict = NULL, *wr = NULL;
...@@ -637,15 +636,14 @@ _local_create_dummy(localobject *self) ...@@ -637,15 +636,14 @@ _local_create_dummy(localobject *self)
goto err; goto err;
Py_CLEAR(dummy); Py_CLEAR(dummy);
Py_CLEAR(self->dict); Py_DECREF(ldict);
self->dict = ldict; return ldict;
return 0;
err: err:
Py_XDECREF(ldict); Py_XDECREF(ldict);
Py_XDECREF(wr); Py_XDECREF(wr);
Py_XDECREF(dummy); Py_XDECREF(dummy);
return -1; return NULL;
} }
static PyObject * static PyObject *
...@@ -691,7 +689,7 @@ local_new(PyTypeObject *type, PyObject *args, PyObject *kw) ...@@ -691,7 +689,7 @@ local_new(PyTypeObject *type, PyObject *args, PyObject *kw)
if (self->wr_callback == NULL) if (self->wr_callback == NULL)
goto err; goto err;
if (_local_create_dummy(self) < 0) if (_local_create_dummy(self) == NULL)
goto err; goto err;
return (PyObject *)self; return (PyObject *)self;
...@@ -707,7 +705,6 @@ local_traverse(localobject *self, visitproc visit, void *arg) ...@@ -707,7 +705,6 @@ local_traverse(localobject *self, visitproc visit, void *arg)
Py_VISIT(self->args); Py_VISIT(self->args);
Py_VISIT(self->kw); Py_VISIT(self->kw);
Py_VISIT(self->dummies); Py_VISIT(self->dummies);
Py_VISIT(self->dict);
return 0; return 0;
} }
...@@ -718,7 +715,6 @@ local_clear(localobject *self) ...@@ -718,7 +715,6 @@ local_clear(localobject *self)
Py_CLEAR(self->args); Py_CLEAR(self->args);
Py_CLEAR(self->kw); Py_CLEAR(self->kw);
Py_CLEAR(self->dummies); Py_CLEAR(self->dummies);
Py_CLEAR(self->dict);
Py_CLEAR(self->wr_callback); Py_CLEAR(self->wr_callback);
/* Remove all strong references to dummies from the thread states */ /* Remove all strong references to dummies from the thread states */
if (self->key if (self->key
...@@ -764,9 +760,9 @@ _ldict(localobject *self) ...@@ -764,9 +760,9 @@ _ldict(localobject *self)
dummy = PyDict_GetItem(tdict, self->key); dummy = PyDict_GetItem(tdict, self->key);
if (dummy == NULL) { if (dummy == NULL) {
if (_local_create_dummy(self) < 0) ldict = _local_create_dummy(self);
if (ldict == NULL)
return NULL; return NULL;
ldict = self->dict;
if (Py_TYPE(self)->tp_init != PyBaseObject_Type.tp_init && if (Py_TYPE(self)->tp_init != PyBaseObject_Type.tp_init &&
Py_TYPE(self)->tp_init((PyObject*)self, Py_TYPE(self)->tp_init((PyObject*)self,
...@@ -783,14 +779,6 @@ _ldict(localobject *self) ...@@ -783,14 +779,6 @@ _ldict(localobject *self)
ldict = ((localdummyobject *) dummy)->localdict; ldict = ((localdummyobject *) dummy)->localdict;
} }
/* The call to tp_init above may have caused another thread to run.
Install our ldict again. */
if (self->dict != ldict) {
Py_INCREF(ldict);
Py_CLEAR(self->dict);
self->dict = ldict;
}
return ldict; return ldict;
} }
...@@ -798,29 +786,25 @@ static int ...@@ -798,29 +786,25 @@ static int
local_setattro(localobject *self, PyObject *name, PyObject *v) local_setattro(localobject *self, PyObject *name, PyObject *v)
{ {
PyObject *ldict; PyObject *ldict;
int r;
ldict = _ldict(self); ldict = _ldict(self);
if (ldict == NULL) if (ldict == NULL)
return -1; return -1;
return PyObject_GenericSetAttr((PyObject *)self, name, v); r = PyObject_RichCompareBool(name, str_dict, Py_EQ);
} if (r == 1) {
PyErr_Format(PyExc_AttributeError,
"'%.50s' object attribute '%U' is read-only",
Py_TYPE(self)->tp_name, name);
return -1;
}
if (r == -1)
return -1;
static PyObject * return _PyObject_GenericSetAttrWithDict((PyObject *)self, name, v, ldict);
local_getdict(localobject *self, void *closure)
{
PyObject *ldict;
ldict = _ldict(self);
Py_XINCREF(ldict);
return ldict;
} }
static PyGetSetDef local_getset[] = {
{"__dict__", (getter)local_getdict, (setter)NULL,
"Local-data dictionary", NULL},
{NULL} /* Sentinel */
};
static PyObject *local_getattro(localobject *, PyObject *); static PyObject *local_getattro(localobject *, PyObject *);
static PyTypeObject localtype = { static PyTypeObject localtype = {
...@@ -854,12 +838,12 @@ static PyTypeObject localtype = { ...@@ -854,12 +838,12 @@ static PyTypeObject localtype = {
/* tp_iternext */ 0, /* tp_iternext */ 0,
/* tp_methods */ 0, /* tp_methods */ 0,
/* tp_members */ 0, /* tp_members */ 0,
/* tp_getset */ local_getset, /* tp_getset */ 0,
/* tp_base */ 0, /* tp_base */ 0,
/* tp_dict */ 0, /* internal use */ /* tp_dict */ 0, /* internal use */
/* tp_descr_get */ 0, /* tp_descr_get */ 0,
/* tp_descr_set */ 0, /* tp_descr_set */ 0,
/* tp_dictoffset */ offsetof(localobject, dict), /* tp_dictoffset */ 0,
/* tp_init */ 0, /* tp_init */ 0,
/* tp_alloc */ 0, /* tp_alloc */ 0,
/* tp_new */ local_new, /* tp_new */ local_new,
...@@ -871,20 +855,29 @@ static PyObject * ...@@ -871,20 +855,29 @@ static PyObject *
local_getattro(localobject *self, PyObject *name) local_getattro(localobject *self, PyObject *name)
{ {
PyObject *ldict, *value; PyObject *ldict, *value;
int r;
ldict = _ldict(self); ldict = _ldict(self);
if (ldict == NULL) if (ldict == NULL)
return NULL; return NULL;
r = PyObject_RichCompareBool(name, str_dict, Py_EQ);
if (r == 1) {
Py_INCREF(ldict);
return ldict;
}
if (r == -1)
return NULL;
if (Py_TYPE(self) != &localtype) if (Py_TYPE(self) != &localtype)
/* use generic lookup for subtypes */ /* use generic lookup for subtypes */
return PyObject_GenericGetAttr((PyObject *)self, name); return _PyObject_GenericGetAttrWithDict((PyObject *)self, name, ldict);
/* Optimization: just look in dict ourselves */ /* Optimization: just look in dict ourselves */
value = PyDict_GetItem(ldict, name); value = PyDict_GetItem(ldict, name);
if (value == NULL) if (value == NULL)
/* Fall back on generic to get __class__ and __dict__ */ /* Fall back on generic to get __class__ and __dict__ */
return PyObject_GenericGetAttr((PyObject *)self, name); return _PyObject_GenericGetAttrWithDict((PyObject *)self, name, ldict);
Py_INCREF(value); Py_INCREF(value);
return value; return value;
...@@ -909,8 +902,6 @@ _localdummy_destroyed(PyObject *localweakref, PyObject *dummyweakref) ...@@ -909,8 +902,6 @@ _localdummy_destroyed(PyObject *localweakref, PyObject *dummyweakref)
PyObject *ldict; PyObject *ldict;
ldict = PyDict_GetItem(self->dummies, dummyweakref); ldict = PyDict_GetItem(self->dummies, dummyweakref);
if (ldict != NULL) { if (ldict != NULL) {
if (ldict == self->dict)
Py_CLEAR(self->dict);
PyDict_DelItem(self->dummies, dummyweakref); PyDict_DelItem(self->dummies, dummyweakref);
} }
if (PyErr_Occurred()) if (PyErr_Occurred())
...@@ -1278,6 +1269,10 @@ PyInit__thread(void) ...@@ -1278,6 +1269,10 @@ PyInit__thread(void)
nb_threads = 0; nb_threads = 0;
str_dict = PyUnicode_InternFromString("__dict__");
if (str_dict == NULL)
return NULL;
/* Initialize the C thread library */ /* Initialize the C thread library */
PyThread_init_thread(); PyThread_init_thread();
return m; return m;
......
...@@ -954,7 +954,7 @@ _PyObject_NextNotImplemented(PyObject *self) ...@@ -954,7 +954,7 @@ _PyObject_NextNotImplemented(PyObject *self)
/* Generic GetAttr functions - put these in your tp_[gs]etattro slot */ /* Generic GetAttr functions - put these in your tp_[gs]etattro slot */
PyObject * PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name) _PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict)
{ {
PyTypeObject *tp = Py_TYPE(obj); PyTypeObject *tp = Py_TYPE(obj);
PyObject *descr = NULL; PyObject *descr = NULL;
...@@ -990,36 +990,37 @@ PyObject_GenericGetAttr(PyObject *obj, PyObject *name) ...@@ -990,36 +990,37 @@ PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
} }
} }
/* Inline _PyObject_GetDictPtr */ if (dict == NULL) {
dictoffset = tp->tp_dictoffset; /* Inline _PyObject_GetDictPtr */
if (dictoffset != 0) { dictoffset = tp->tp_dictoffset;
PyObject *dict; if (dictoffset != 0) {
if (dictoffset < 0) { if (dictoffset < 0) {
Py_ssize_t tsize; Py_ssize_t tsize;
size_t size; size_t size;
tsize = ((PyVarObject *)obj)->ob_size; tsize = ((PyVarObject *)obj)->ob_size;
if (tsize < 0) if (tsize < 0)
tsize = -tsize; tsize = -tsize;
size = _PyObject_VAR_SIZE(tp, tsize); size = _PyObject_VAR_SIZE(tp, tsize);
dictoffset += (long)size; dictoffset += (long)size;
assert(dictoffset > 0); assert(dictoffset > 0);
assert(dictoffset % SIZEOF_VOID_P == 0); assert(dictoffset % SIZEOF_VOID_P == 0);
}
dictptr = (PyObject **) ((char *)obj + dictoffset);
dict = *dictptr;
if (dict != NULL) {
Py_INCREF(dict);
res = PyDict_GetItem(dict, name);
if (res != NULL) {
Py_INCREF(res);
Py_XDECREF(descr);
Py_DECREF(dict);
goto done;
} }
dictptr = (PyObject **) ((char *)obj + dictoffset);
dict = *dictptr;
}
}
if (dict != NULL) {
Py_INCREF(dict);
res = PyDict_GetItem(dict, name);
if (res != NULL) {
Py_INCREF(res);
Py_XDECREF(descr);
Py_DECREF(dict); Py_DECREF(dict);
goto done;
} }
Py_DECREF(dict);
} }
if (f != NULL) { if (f != NULL) {
...@@ -1042,8 +1043,15 @@ PyObject_GenericGetAttr(PyObject *obj, PyObject *name) ...@@ -1042,8 +1043,15 @@ PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
return res; return res;
} }
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
return _PyObject_GenericGetAttrWithDict(obj, name, NULL);
}
int int
PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value) _PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name,
PyObject *value, PyObject *dict)
{ {
PyTypeObject *tp = Py_TYPE(obj); PyTypeObject *tp = Py_TYPE(obj);
PyObject *descr; PyObject *descr;
...@@ -1075,27 +1083,29 @@ PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value) ...@@ -1075,27 +1083,29 @@ PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value)
} }
} }
dictptr = _PyObject_GetDictPtr(obj); if (dict == NULL) {
if (dictptr != NULL) { dictptr = _PyObject_GetDictPtr(obj);
PyObject *dict = *dictptr; if (dictptr != NULL) {
if (dict == NULL && value != NULL) { dict = *dictptr;
dict = PyDict_New(); if (dict == NULL && value != NULL) {
if (dict == NULL) dict = PyDict_New();
goto done; if (dict == NULL)
*dictptr = dict; goto done;
} *dictptr = dict;
if (dict != NULL) { }
Py_INCREF(dict);
if (value == NULL)
res = PyDict_DelItem(dict, name);
else
res = PyDict_SetItem(dict, name, value);
if (res < 0 && PyErr_ExceptionMatches(PyExc_KeyError))
PyErr_SetObject(PyExc_AttributeError, name);
Py_DECREF(dict);
goto done;
} }
} }
if (dict != NULL) {
Py_INCREF(dict);
if (value == NULL)
res = PyDict_DelItem(dict, name);
else
res = PyDict_SetItem(dict, name, value);
if (res < 0 && PyErr_ExceptionMatches(PyExc_KeyError))
PyErr_SetObject(PyExc_AttributeError, name);
Py_DECREF(dict);
goto done;
}
if (f != NULL) { if (f != NULL) {
res = f(descr, obj, value); res = f(descr, obj, value);
...@@ -1117,6 +1127,13 @@ PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value) ...@@ -1117,6 +1127,13 @@ PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value)
return res; return res;
} }
int
PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value)
{
return _PyObject_GenericSetAttrWithDict(obj, name, value, NULL);
}
/* Test a value used as condition, e.g., in a for or if statement. /* Test a value used as condition, e.g., in a for or if statement.
Return -1 if an error occurred */ Return -1 if an error occurred */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment