summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorJim Fulton <jim@zope.com>2004-07-14 19:11:50 (GMT)
committerJim Fulton <jim@zope.com>2004-07-14 19:11:50 (GMT)
commitd15dc06df062fdf0fe8badec2982c6c5e0e28eb0 (patch)
treee0295c2c22fa7c2e702b37a1d7afd77547a8894f /Lib
parente827437f45510d9cdd1e7fa561da8084f69ca698 (diff)
downloadcpython-d15dc06df062fdf0fe8badec2982c6c5e0e28eb0.zip
cpython-d15dc06df062fdf0fe8badec2982c6c5e0e28eb0.tar.gz
cpython-d15dc06df062fdf0fe8badec2982c6c5e0e28eb0.tar.bz2
Implemented thread-local data as proposed on python-dev:
http://mail.python.org/pipermail/python-dev/2004-June/045785.html
Diffstat (limited to 'Lib')
-rw-r--r--Lib/_threading_local.py237
-rw-r--r--Lib/dummy_threading.py20
-rw-r--r--Lib/test/test_threading_local.py26
-rw-r--r--Lib/threading.py10
4 files changed, 292 insertions, 1 deletions
diff --git a/Lib/_threading_local.py b/Lib/_threading_local.py
new file mode 100644
index 0000000..3509493
--- /dev/null
+++ b/Lib/_threading_local.py
@@ -0,0 +1,237 @@
+"""Thread-local objects
+
+(Note that this module provides a Python version of thread
+ threading.local class. Deoending on the version of Python you're
+ using, there may be a faster one available. You should always import
+ the local class from threading.)
+
+Thread-local objects support the management of thread-local data.
+If you have data that you want to be local to a thread, simply create
+a thread-local object and use it's attributes:
+
+ >>> mydata = local()
+ >>> mydata.number = 42
+ >>> mydata.number
+ 42
+
+You can also access the local-object's dictionary:
+
+ >>> mydata.__dict__
+ {'number': 42}
+ >>> mydata.__dict__.setdefault('widgets', [])
+ []
+ >>> mydata.widgets
+ []
+
+What's important about thread-local objects is that their data are
+local to a thread. If we access the data in a different thread:
+
+ >>> log = []
+ >>> def f():
+ ... items = mydata.__dict__.items()
+ ... items.sort()
+ ... log.append(items)
+ ... mydata.number = 11
+ ... log.append(mydata.number)
+
+ >>> import threading
+ >>> thread = threading.Thread(target=f)
+ >>> thread.start()
+ >>> thread.join()
+ >>> log
+ [[], 11]
+
+we get different data. Furthermore, changes made in the other thread
+don't affect data seen in this thread:
+
+ >>> mydata.number
+ 42
+
+Of course, values you get from a local object, including a __dict__
+attribute, are for whatever thread was current at the time the
+attribute was read. For that reason, you generally don't want to save
+these values across threads, as they apply only to the thread they
+came from.
+
+You can create custom local objects by subclassing the local class:
+
+ >>> class MyLocal(local):
+ ... number = 2
+ ... initialized = False
+ ... def __init__(self, **kw):
+ ... if self.initialized:
+ ... raise SystemError('__init__ called too many times')
+ ... self.initialized = True
+ ... self.__dict__.update(kw)
+ ... def squared(self):
+ ... return self.number ** 2
+
+This can be useful to support default values, methods and
+initialization. Note that if you define an __init__ method, it will be
+called each time the local object is used in a separate thread. This
+is necessary to initialize each thread's dictionary.
+
+Now if we create a local object:
+
+ >>> mydata = MyLocal(color='red')
+
+Now we have a default number:
+
+ >>> mydata.number
+ 2
+
+an initial color:
+
+ >>> mydata.color
+ 'red'
+ >>> del mydata.color
+
+And a method that operates on the data:
+
+ >>> mydata.squared()
+ 4
+
+As before, we can access the data in a separate thread:
+
+ >>> log = []
+ >>> thread = threading.Thread(target=f)
+ >>> thread.start()
+ >>> thread.join()
+ >>> log
+ [[('color', 'red'), ('initialized', True)], 11]
+
+without effecting this threads data:
+
+ >>> mydata.number
+ 2
+ >>> mydata.color
+ Traceback (most recent call last):
+ ...
+ AttributeError: 'MyLocal' object has no attribute 'color'
+
+Note that subclasses can define slots, but they are not thread
+local. They are shared across threads:
+
+ >>> class MyLocal(local):
+ ... __slots__ = 'number'
+
+ >>> mydata = MyLocal()
+ >>> mydata.number = 42
+ >>> mydata.color = 'red'
+
+So, the separate thread:
+
+ >>> thread = threading.Thread(target=f)
+ >>> thread.start()
+ >>> thread.join()
+
+affects what we see:
+
+ >>> mydata.number
+ 11
+
+>>> del mydata
+"""
+
+# Threading import is at end
+
+class _localbase(object):
+ __slots__ = '_local__key', '_local__args', '_local__lock'
+
+ def __new__(cls, *args, **kw):
+ self = object.__new__(cls)
+ key = '_local__key', 'thread.local.' + str(id(self))
+ object.__setattr__(self, '_local__key', key)
+ object.__setattr__(self, '_local__args', (args, kw))
+ object.__setattr__(self, '_local__lock', RLock())
+
+ if args or kw and (cls.__init__ is object.__init__):
+ raise TypeError("Initialization arguments are not supported")
+
+ # We need to create the thread dict in anticipation of
+ # __init__ being called, to make sire we don't cal it
+ # again ourselves.
+ dict = object.__getattribute__(self, '__dict__')
+ currentThread().__dict__[key] = dict
+
+ return self
+
+def _patch(self):
+ key = object.__getattribute__(self, '_local__key')
+ d = currentThread().__dict__.get(key)
+ if d is None:
+ d = {}
+ currentThread().__dict__[key] = d
+ object.__setattr__(self, '__dict__', d)
+
+ # we have a new instance dict, so call out __init__ if we have
+ # one
+ cls = type(self)
+ if cls.__init__ is not object.__init__:
+ args, kw = object.__getattribute__(self, '_local__args')
+ cls.__init__(self, *args, **kw)
+ else:
+ object.__setattr__(self, '__dict__', d)
+
+class local(_localbase):
+
+ def __getattribute__(self, name):
+ lock = object.__getattribute__(self, '_local__lock')
+ lock.acquire()
+ try:
+ _patch(self)
+ return object.__getattribute__(self, name)
+ finally:
+ lock.release()
+
+ def __setattr__(self, name, value):
+ lock = object.__getattribute__(self, '_local__lock')
+ lock.acquire()
+ try:
+ _patch(self)
+ return object.__setattr__(self, name, value)
+ finally:
+ lock.release()
+
+ def __delattr__(self, name):
+ lock = object.__getattribute__(self, '_local__lock')
+ lock.acquire()
+ try:
+ _patch(self)
+ return object.__delattr__(self, name)
+ finally:
+ lock.release()
+
+
+ def __del__():
+ threading_enumerate = enumerate
+ __getattribute__ = object.__getattribute__
+
+ def __del__(self):
+ key = __getattribute__(self, '_local__key')
+
+ try:
+ threads = list(threading_enumerate())
+ except:
+ # if enumerate fails, as it seems to do during
+ # shutdown, we'll skip cleanup under the assumption
+ # that there is nothing to clean up
+ return
+
+ for thread in threads:
+ try:
+ __dict__ = thread.__dict__
+ except AttributeError:
+ # Thread is dying, rest in peace
+ continue
+
+ if key in __dict__:
+ try:
+ del __dict__[key]
+ except KeyError:
+ pass # didn't have nything in this thread
+
+ return __del__
+ __del__ = __del__()
+
+from threading import currentThread, enumerate, RLock
diff --git a/Lib/dummy_threading.py b/Lib/dummy_threading.py
index 2e070aa..4d0b744 100644
--- a/Lib/dummy_threading.py
+++ b/Lib/dummy_threading.py
@@ -18,6 +18,7 @@ import dummy_thread
# Declaring now so as to not have to nest ``try``s to get proper clean-up.
holding_thread = False
holding_threading = False
+holding__threading_local = False
try:
# Could have checked if ``thread`` was not in sys.modules and gone
@@ -37,20 +38,39 @@ try:
held_threading = sys_modules['threading']
holding_threading = True
del sys_modules['threading']
+
+ if '_threading_local' in sys_modules:
+ # If ``_threading_local`` is already imported, might as well prevent
+ # trying to import it more than needed by saving it if it is
+ # already imported before deleting it.
+ held__threading_local = sys_modules['_threading_local']
+ holding__threading_local = True
+ del sys_modules['_threading_local']
+
import threading
# Need a copy of the code kept somewhere...
sys_modules['_dummy_threading'] = sys_modules['threading']
del sys_modules['threading']
+ sys_modules['_dummy__threading_local'] = sys_modules['_threading_local']
+ del sys_modules['_threading_local']
from _dummy_threading import *
from _dummy_threading import __all__
finally:
# Put back ``threading`` if we overwrote earlier
+
if holding_threading:
sys_modules['threading'] = held_threading
del held_threading
del holding_threading
+ # Put back ``_threading_local`` if we overwrote earlier
+
+ if holding__threading_local:
+ sys_modules['_threading_local'] = held__threading_local
+ del held__threading_local
+ del holding__threading_local
+
# Put back ``thread`` if we overwrote, else del the entry we made
if holding_thread:
sys_modules['thread'] = held_thread
diff --git a/Lib/test/test_threading_local.py b/Lib/test/test_threading_local.py
new file mode 100644
index 0000000..1258455
--- /dev/null
+++ b/Lib/test/test_threading_local.py
@@ -0,0 +1,26 @@
+import unittest
+from doctest import DocTestSuite
+from test import test_support
+
+def test_main():
+ suite = DocTestSuite('_threading_local')
+
+ try:
+ from thread import _local
+ except ImportError:
+ pass
+ else:
+ import _threading_local
+ local_orig = _threading_local.local
+ def setUp():
+ _threading_local.local = _local
+ def tearDown():
+ _threading_local.local = local_orig
+ suite.addTest(DocTestSuite('_threading_local',
+ setUp=setUp, tearDown=tearDown)
+ )
+
+ test_support.run_suite(suite)
+
+if __name__ == '__main__':
+ test_main()
diff --git a/Lib/threading.py b/Lib/threading.py
index e70c61b..c485524 100644
--- a/Lib/threading.py
+++ b/Lib/threading.py
@@ -15,7 +15,7 @@ from collections import deque
# Rename some stuff so "from threading import *" is safe
__all__ = ['activeCount', 'Condition', 'currentThread', 'enumerate', 'Event',
'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread',
- 'Timer', 'setprofile', 'settrace']
+ 'Timer', 'setprofile', 'settrace', 'local']
_start_new_thread = thread.start_new_thread
_allocate_lock = thread.allocate_lock
@@ -661,6 +661,14 @@ def enumerate():
_MainThread()
+# get thread-local implementation, either from the thread
+# module, or from the python fallback
+
+try:
+ from thread import _local as local
+except ImportError:
+ from _threading_local import local
+
# Self-test code