Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
allendowney
GitHub Repository: allendowney/cpython
Path: blob/main/Lib/_threading_local.py
12 views
1
"""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
8
Thread-local objects support the management of thread-local data.
9
If you have data that you want to be local to a thread, simply create
10
a thread-local object and use its attributes:
11
12
>>> mydata = local()
13
>>> mydata.number = 42
14
>>> mydata.number
15
42
16
17
You 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
26
What's important about thread-local objects is that their data are
27
local 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
43
we get different data. Furthermore, changes made in the other thread
44
don't affect data seen in this thread:
45
46
>>> mydata.number
47
42
48
49
Of course, values you get from a local object, including a __dict__
50
attribute, are for whatever thread was current at the time the
51
attribute was read. For that reason, you generally don't want to save
52
these values across threads, as they apply only to the thread they
53
came from.
54
55
You 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
64
This can be useful to support default values, methods and
65
initialization. Note that if you define an __init__ method, it will be
66
called each time the local object is used in a separate thread. This
67
is necessary to initialize each thread's dictionary.
68
69
Now if we create a local object:
70
71
>>> mydata = MyLocal(color='red')
72
73
Now we have a default number:
74
75
>>> mydata.number
76
2
77
78
an initial color:
79
80
>>> mydata.color
81
'red'
82
>>> del mydata.color
83
84
And a method that operates on the data:
85
86
>>> mydata.squared()
87
4
88
89
As 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
98
without 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
107
Note that subclasses can define slots, but they are not thread
108
local. 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
117
So, the separate thread:
118
119
>>> thread = threading.Thread(target=f)
120
>>> thread.start()
121
>>> thread.join()
122
123
affects what we see:
124
125
>>> mydata.number
126
11
127
128
>>> del mydata
129
"""
130
131
from weakref import ref
132
from 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
146
class _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
191
def _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
204
class 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
242
from threading import current_thread, RLock
243
244