class Concurrent::RubyThreadLocalVar
@!visibility private @!macro internal_implementation_note
Constants
- FREE
Each thread has a (lazily initialized) array of thread-local variable values Each time a new thread-local var is created, we allocate an “index” for it For example, if the allocated index is 1, that means slot #1 in EVERY
thread's thread-local array will be used for the value of that TLV
The good thing about using a per-THREAD structure to hold values, rather
than a per-TLV structure, is that no synchronization is needed when reading and writing those values (since the structure is only ever accessed by a single thread)
Of course, when a TLV is GC'd, 1) we need to recover its index for use
by other new TLVs (otherwise the thread-local arrays could get bigger and bigger with time), and 2) we need to null out all the references held in the now-unused slots (both to avoid blocking GC of those objects, and also to prevent "stale" values from being passed on to a new TLV when the index is reused)
Because we need to null out freed slots, we need to keep references to
ALL the thread-local arrays -- ARRAYS is for that
But when a Thread is GC'd, we need to drop the reference to its thread-local
array, so we don't leak memory
- LOCK
- THREAD_LOCAL_ARRAYS
Public Class Methods
@!visibility private
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 41 def self.semi_sync(&block) block.call end
Protected Class Methods
@!visibility private
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 111 def self.thread_finalizer(id) proc do semi_sync do # The thread which used this thread-local array is now gone # So don't hold onto a reference to the array (thus blocking GC) THREAD_LOCAL_ARRAYS.delete(id) end end end
@!visibility private
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 94 def self.thread_local_finalizer(index) proc do semi_sync do # The cost of GC'ing a TLV is linear in the number of threads using TLVs # But that is natural! More threads means more storage is used per TLV # So naturally more CPU time is required to free more storage # # DO NOT use each_value which might conflict with new pair assignment # into the hash in #value= method THREAD_LOCAL_ARRAYS.values.each { |array| array[index] = nil } # free index has to be published after the arrays are cleared FREE.push(index) end end end
Public Instance Methods
@!macro thread_local_var_method_get
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 54 def value if (array = get_threadlocal_array) value = array[@index] if value.nil? default elsif value.equal?(NULL) nil else value end else default end end
@!macro thread_local_var_method_set
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 70 def value=(value) me = Thread.current # We could keep the thread-local arrays in a hash, keyed by Thread # But why? That would require locking # Using Ruby's built-in thread-local storage is faster unless (array = get_threadlocal_array(me)) array = set_threadlocal_array([], me) self.class.semi_sync { THREAD_LOCAL_ARRAYS[array.object_id] = array } ObjectSpace.define_finalizer(me, self.class.thread_finalizer(array.object_id)) end array[@index] = (value.nil? ? NULL : value) value end
Protected Instance Methods
@!visibility private
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 87 def allocate_storage @index = FREE.pop || next_index ObjectSpace.define_finalizer(self, self.class.thread_local_finalizer(@index)) end
Private Instance Methods
@!visibility private
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 173 def get_default if @default_block raise "Cannot use default_for with default block" else @default end end
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 136 def get_threadlocal_array(thread = Thread.current) thread.thread_variable_get(:__threadlocal_array__) end
noinspection RubyClassVariableUsageInspection
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 126 def next_index LOCK.synchronize do result = @@next @@next += 1 result end end
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 140 def set_threadlocal_array(array, thread = Thread.current) thread.thread_variable_set(:__threadlocal_array__, array) end
This exists only for use in testing @!visibility private
# File lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb, line 157 def value_for(thread) if (array = get_threadlocal_array(thread)) value = array[@index] if value.nil? get_default elsif value.equal?(NULL) nil else value end else get_default end end