1
- require 'concurrent/atomic/thread_local_var/weak_key_map '
1
+ require 'thread '
2
2
3
3
module Concurrent
4
4
@@ -70,7 +70,7 @@ def value
70
70
# @!macro [attach] thread_local_var_method_set
71
71
#
72
72
# Sets the current thread's copy of this thread-local variable to the specified value.
73
- #
73
+ #
74
74
# @param [Object] value the value to set
75
75
# @return [Object] the new value
76
76
def value = ( value )
@@ -81,7 +81,7 @@ def value=(value)
81
81
#
82
82
# Bind the given value to thread local storage during
83
83
# execution of the given block.
84
- #
84
+ #
85
85
# @param [Object] value the value to bind
86
86
# @yield the operation to be performed with the bound variable
87
87
# @return [Object] the value
@@ -119,29 +119,129 @@ def set(value)
119
119
# @!macro internal_implementation_note
120
120
class RubyThreadLocalVar < AbstractThreadLocalVar
121
121
122
+ # Each thread has a (lazily initialized) array of thread-local variable values
123
+ # Each time a new thread-local var is created, we allocate an "index" for it
124
+ # For example, if the allocated index is 1, that means slot #1 in EVERY
125
+ # thread's thread-local array will be used for the value of that TLV
126
+ #
127
+ # The good thing about using a per-THREAD structure to hold values, rather
128
+ # than a per-TLV structure, is that no synchronization is needed when
129
+ # reading and writing those values (since the structure is only ever
130
+ # accessed by a single thread)
131
+ #
132
+ # Of course, when a TLV is GC'd, 1) we need to recover its index for use
133
+ # by other new TLVs (otherwise the thread-local arrays could get bigger
134
+ # and bigger with time), and 2) we need to null out all the references
135
+ # held in the now-unused slots (both to avoid blocking GC of those objects,
136
+ # and also to prevent "stale" values from being passed on to a new TLV
137
+ # when the index is reused)
138
+ # Because we need to null out freed slots, we need to keep references to
139
+ # ALL the thread-local arrays -- ARRAYS is for that
140
+ # But when a Thread is GC'd, we need to drop the reference to its thread-local
141
+ # array, so we don't leak memory
142
+
143
+ FREE = [ ]
144
+ LOCK = Mutex . new
145
+ ARRAYS = { } # used as a hash set
146
+ @@next = 0
147
+
122
148
protected
123
149
124
150
# @!visibility private
125
- def allocate_storage
126
- @storage = WeakKeyMap . new
151
+ def self . threadlocal_finalizer ( index )
152
+ proc do
153
+ LOCK . synchronize do
154
+ FREE . push ( index )
155
+ # The cost of GC'ing a TLV is linear in the number of threads using TLVs
156
+ # But that is natural! More threads means more storage is used per TLV
157
+ # So naturally more CPU time is required to free more storage
158
+ ARRAYS . each_value do |array |
159
+ array [ index ] = nil
160
+ end
161
+ end
162
+ end
127
163
end
128
164
129
165
# @!visibility private
130
- def get
131
- @storage [ Thread . current ]
166
+ def self . thread_finalizer ( array )
167
+ proc do
168
+ LOCK . synchronize do
169
+ # The thread which used this thread-local array is now gone
170
+ # So don't hold onto a reference to the array (thus blocking GC)
171
+ ARRAYS . delete ( array . object_id )
172
+ end
173
+ end
132
174
end
133
175
134
- # @!visibility private
135
- def set ( value )
136
- key = Thread . current
176
+ def allocate_storage
177
+ @index = LOCK . synchronize do
178
+ FREE . pop || begin
179
+ result = @@next
180
+ @@next += 1
181
+ result
182
+ end
183
+ end
184
+ ObjectSpace . define_finalizer ( self , self . class . threadlocal_finalizer ( @index ) )
185
+ end
137
186
138
- @storage [ key ] = value
187
+ public
139
188
189
+ # @!macro [attach] thread_local_var_method_get
190
+ #
191
+ # Returns the value in the current thread's copy of this thread-local variable.
192
+ #
193
+ # @return [Object] the current value
194
+ def value
195
+ if array = Thread . current [ :__threadlocal_array__ ]
196
+ value = array [ @index ]
197
+ if value . nil?
198
+ @default
199
+ elsif value . equal? ( NIL_SENTINEL )
200
+ nil
201
+ else
202
+ value
203
+ end
204
+ else
205
+ @default
206
+ end
207
+ end
208
+
209
+ # @!macro [attach] thread_local_var_method_set
210
+ #
211
+ # Sets the current thread's copy of this thread-local variable to the specified value.
212
+ #
213
+ # @param [Object] value the value to set
214
+ # @return [Object] the new value
215
+ def value = ( value )
216
+ me = Thread . current
217
+ # We could keep the thread-local arrays in a hash, keyed by Thread
218
+ # But why? That would require locking
219
+ # Using Ruby's built-in thread-local storage is faster
220
+ unless array = me [ :__threadlocal_array__ ]
221
+ array = me [ :__threadlocal_array__ ] = [ ]
222
+ LOCK . synchronize { ARRAYS [ array . object_id ] = array }
223
+ ObjectSpace . define_finalizer ( me , self . class . thread_finalizer ( array ) )
224
+ end
225
+ array [ @index ] = ( value . nil? ? NIL_SENTINEL : value )
226
+ value
227
+ end
228
+
229
+ # @!macro [attach] thread_local_var_method_bind
230
+ #
231
+ # Bind the given value to thread local storage during
232
+ # execution of the given block.
233
+ #
234
+ # @param [Object] value the value to bind
235
+ # @yield the operation to be performed with the bound variable
236
+ # @return [Object] the value
237
+ def bind ( value , &block )
140
238
if block_given?
239
+ old_value = self . value
141
240
begin
241
+ self . value = value
142
242
yield
143
243
ensure
144
- @storage . delete ( key )
244
+ self . value = old_value
145
245
end
146
246
end
147
247
end
0 commit comments