blob: b006d76c4e23df7dbf09bc7e668b9eb87e4044af [file] [log] [blame]
Jingwen Chen475b3cc2021-01-05 21:45:16 -05001"""Thread-local objects.
2
3(Note that this module provides a Python version of the threading.local
4 class. Depending on the version of Python you're using, there may be a
5 faster one available. You should always import the `local` class from
6 `threading`.)
7
8Thread-local objects support the management of thread-local data.
9If you have data that you want to be local to a thread, simply create
10a thread-local object and use its attributes:
11
12 >>> mydata = local()
13 >>> mydata.number = 42
14 >>> mydata.number
15 42
16
17You can also access the local-object's dictionary:
18
19 >>> mydata.__dict__
20 {'number': 42}
21 >>> mydata.__dict__.setdefault('widgets', [])
22 []
23 >>> mydata.widgets
24 []
25
26What's important about thread-local objects is that their data are
27local to a thread. If we access the data in a different thread:
28
29 >>> log = []
30 >>> def f():
31 ... items = sorted(mydata.__dict__.items())
32 ... log.append(items)
33 ... mydata.number = 11
34 ... log.append(mydata.number)
35
36 >>> import threading
37 >>> thread = threading.Thread(target=f)
38 >>> thread.start()
39 >>> thread.join()
40 >>> log
41 [[], 11]
42
43we get different data. Furthermore, changes made in the other thread
44don't affect data seen in this thread:
45
46 >>> mydata.number
47 42
48
49Of course, values you get from a local object, including a __dict__
50attribute, are for whatever thread was current at the time the
51attribute was read. For that reason, you generally don't want to save
52these values across threads, as they apply only to the thread they
53came from.
54
55You can create custom local objects by subclassing the local class:
56
57 >>> class MyLocal(local):
58 ... number = 2
59 ... def __init__(self, /, **kw):
60 ... self.__dict__.update(kw)
61 ... def squared(self):
62 ... return self.number ** 2
63
64This can be useful to support default values, methods and
65initialization. Note that if you define an __init__ method, it will be
66called each time the local object is used in a separate thread. This
67is necessary to initialize each thread's dictionary.
68
69Now if we create a local object:
70
71 >>> mydata = MyLocal(color='red')
72
73Now we have a default number:
74
75 >>> mydata.number
76 2
77
78an initial color:
79
80 >>> mydata.color
81 'red'
82 >>> del mydata.color
83
84And a method that operates on the data:
85
86 >>> mydata.squared()
87 4
88
89As before, we can access the data in a separate thread:
90
91 >>> log = []
92 >>> thread = threading.Thread(target=f)
93 >>> thread.start()
94 >>> thread.join()
95 >>> log
96 [[('color', 'red')], 11]
97
98without affecting this thread's data:
99
100 >>> mydata.number
101 2
102 >>> mydata.color
103 Traceback (most recent call last):
104 ...
105 AttributeError: 'MyLocal' object has no attribute 'color'
106
107Note that subclasses can define slots, but they are not thread
108local. They are shared across threads:
109
110 >>> class MyLocal(local):
111 ... __slots__ = 'number'
112
113 >>> mydata = MyLocal()
114 >>> mydata.number = 42
115 >>> mydata.color = 'red'
116
117So, the separate thread:
118
119 >>> thread = threading.Thread(target=f)
120 >>> thread.start()
121 >>> thread.join()
122
123affects what we see:
124
125 >>> mydata.number
126 11
127
128>>> del mydata
129"""
130
131from weakref import ref
132from contextlib import contextmanager
133
134__all__ = ["local"]
135
136# We need to use objects from the threading module, but the threading
137# module may also want to use our `local` class, if support for locals
138# isn't compiled in to the `thread` module. This creates potential problems
139# with circular imports. For that reason, we don't import `threading`
140# until the bottom of this file (a hack sufficient to worm around the
141# potential problems). Note that all platforms on CPython do have support
142# for locals in the `thread` module, and there is no circular import problem
143# then, so problems introduced by fiddling the order of imports here won't
144# manifest.
145
146class _localimpl:
147 """A class managing thread-local dicts"""
148 __slots__ = 'key', 'dicts', 'localargs', 'locallock', '__weakref__'
149
150 def __init__(self):
151 # The key used in the Thread objects' attribute dicts.
152 # We keep it a string for speed but make it unlikely to clash with
153 # a "real" attribute.
154 self.key = '_threading_local._localimpl.' + str(id(self))
155 # { id(Thread) -> (ref(Thread), thread-local dict) }
156 self.dicts = {}
157
158 def get_dict(self):
159 """Return the dict for the current thread. Raises KeyError if none
160 defined."""
161 thread = current_thread()
162 return self.dicts[id(thread)][1]
163
164 def create_dict(self):
165 """Create a new dict for the current thread, and return it."""
166 localdict = {}
167 key = self.key
168 thread = current_thread()
169 idt = id(thread)
170 def local_deleted(_, key=key):
171 # When the localimpl is deleted, remove the thread attribute.
172 thread = wrthread()
173 if thread is not None:
174 del thread.__dict__[key]
175 def thread_deleted(_, idt=idt):
176 # When the thread is deleted, remove the local dict.
177 # Note that this is suboptimal if the thread object gets
178 # caught in a reference loop. We would like to be called
179 # as soon as the OS-level thread ends instead.
180 local = wrlocal()
181 if local is not None:
182 dct = local.dicts.pop(idt)
183 wrlocal = ref(self, local_deleted)
184 wrthread = ref(thread, thread_deleted)
185 thread.__dict__[key] = wrlocal
186 self.dicts[idt] = wrthread, localdict
187 return localdict
188
189
190@contextmanager
191def _patch(self):
192 impl = object.__getattribute__(self, '_local__impl')
193 try:
194 dct = impl.get_dict()
195 except KeyError:
196 dct = impl.create_dict()
197 args, kw = impl.localargs
198 self.__init__(*args, **kw)
199 with impl.locallock:
200 object.__setattr__(self, '__dict__', dct)
201 yield
202
203
204class local:
205 __slots__ = '_local__impl', '__dict__'
206
207 def __new__(cls, /, *args, **kw):
208 if (args or kw) and (cls.__init__ is object.__init__):
209 raise TypeError("Initialization arguments are not supported")
210 self = object.__new__(cls)
211 impl = _localimpl()
212 impl.localargs = (args, kw)
213 impl.locallock = RLock()
214 object.__setattr__(self, '_local__impl', impl)
215 # We need to create the thread dict in anticipation of
216 # __init__ being called, to make sure we don't call it
217 # again ourselves.
218 impl.create_dict()
219 return self
220
221 def __getattribute__(self, name):
222 with _patch(self):
223 return object.__getattribute__(self, name)
224
225 def __setattr__(self, name, value):
226 if name == '__dict__':
227 raise AttributeError(
228 "%r object attribute '__dict__' is read-only"
229 % self.__class__.__name__)
230 with _patch(self):
231 return object.__setattr__(self, name, value)
232
233 def __delattr__(self, name):
234 if name == '__dict__':
235 raise AttributeError(
236 "%r object attribute '__dict__' is read-only"
237 % self.__class__.__name__)
238 with _patch(self):
239 return object.__delattr__(self, name)
240
241
242from threading import current_thread, RLock