1- require 'concurrent/atomic/thread_local_var/weak_key_map '
1+ require 'thread '
22
33module Concurrent
44
@@ -70,7 +70,7 @@ def value
7070 # @!macro [attach] thread_local_var_method_set
7171 #
7272 # Sets the current thread's copy of this thread-local variable to the specified value.
73- #
73+ #
7474 # @param [Object] value the value to set
7575 # @return [Object] the new value
7676 def value = ( value )
@@ -81,7 +81,7 @@ def value=(value)
8181 #
8282 # Bind the given value to thread local storage during
8383 # execution of the given block.
84- #
84+ #
8585 # @param [Object] value the value to bind
8686 # @yield the operation to be performed with the bound variable
8787 # @return [Object] the value
@@ -119,29 +119,129 @@ def set(value)
119119 # @!macro internal_implementation_note
120120 class RubyThreadLocalVar < AbstractThreadLocalVar
121121
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+
122148 protected
123149
124150 # @!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
127163 end
128164
129165 # @!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
132174 end
133175
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
137186
138- @storage [ key ] = value
187+ public
139188
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 )
140238 if block_given?
239+ old_value = self . value
141240 begin
241+ self . value = value
142242 yield
143243 ensure
144- @storage . delete ( key )
244+ self . value = old_value
145245 end
146246 end
147247 end
0 commit comments