Skip to content

Commit 5946545

Browse files
committed
Moved thread local var classes into own files.
1 parent cf131ae commit 5946545

File tree

4 files changed

+325
-294
lines changed

4 files changed

+325
-294
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
module Concurrent
2+
3+
# @!macro thread_local_var
4+
# @!macro internal_implementation_note
5+
# @!visibility private
6+
class AbstractThreadLocalVar
7+
8+
# @!visibility private
9+
NIL_SENTINEL = Object.new
10+
private_constant :NIL_SENTINEL
11+
12+
# @!macro thread_local_var_method_initialize
13+
def initialize(default = nil)
14+
@default = default
15+
allocate_storage
16+
end
17+
18+
# @!macro thread_local_var_method_get
19+
def value
20+
raise NotImplementedError
21+
end
22+
23+
# @!macro thread_local_var_method_set
24+
def value=(value)
25+
raise NotImplementedError
26+
end
27+
28+
# @!macro thread_local_var_method_bind
29+
def bind(value, &block)
30+
raise NotImplementedError
31+
end
32+
33+
protected
34+
35+
# @!visibility private
36+
def allocate_storage
37+
raise NotImplementedError
38+
end
39+
end
40+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
require 'concurrent/atomic/abstract_thread_local_var'
2+
3+
if Concurrent.on_jruby?
4+
5+
module Concurrent
6+
7+
# @!visibility private
8+
# @!macro internal_implementation_note
9+
class JavaThreadLocalVar < AbstractThreadLocalVar
10+
11+
# @!macro thread_local_var_method_get
12+
def value
13+
value = @var.get
14+
15+
if value.nil?
16+
@default
17+
elsif value == NIL_SENTINEL
18+
nil
19+
else
20+
value
21+
end
22+
end
23+
24+
# @!macro thread_local_var_method_set
25+
def value=(value)
26+
@var.set(value)
27+
end
28+
29+
# @!macro thread_local_var_method_bind
30+
def bind(value, &block)
31+
if block_given?
32+
old_value = @var.get
33+
begin
34+
@var.set(value)
35+
yield
36+
ensure
37+
@var.set(old_value)
38+
end
39+
end
40+
end
41+
42+
protected
43+
44+
# @!visibility private
45+
def allocate_storage
46+
@var = java.lang.ThreadLocal.new
47+
end
48+
end
49+
end
50+
end
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
require 'thread'
2+
require 'concurrent/atomic/abstract_thread_local_var'
3+
4+
module Concurrent
5+
6+
# @!visibility private
7+
# @!macro internal_implementation_note
8+
class RubyThreadLocalVar < AbstractThreadLocalVar
9+
10+
# Each thread has a (lazily initialized) array of thread-local variable values
11+
# Each time a new thread-local var is created, we allocate an "index" for it
12+
# For example, if the allocated index is 1, that means slot #1 in EVERY
13+
# thread's thread-local array will be used for the value of that TLV
14+
#
15+
# The good thing about using a per-THREAD structure to hold values, rather
16+
# than a per-TLV structure, is that no synchronization is needed when
17+
# reading and writing those values (since the structure is only ever
18+
# accessed by a single thread)
19+
#
20+
# Of course, when a TLV is GC'd, 1) we need to recover its index for use
21+
# by other new TLVs (otherwise the thread-local arrays could get bigger
22+
# and bigger with time), and 2) we need to null out all the references
23+
# held in the now-unused slots (both to avoid blocking GC of those objects,
24+
# and also to prevent "stale" values from being passed on to a new TLV
25+
# when the index is reused)
26+
# Because we need to null out freed slots, we need to keep references to
27+
# ALL the thread-local arrays -- ARRAYS is for that
28+
# But when a Thread is GC'd, we need to drop the reference to its thread-local
29+
# array, so we don't leak memory
30+
31+
# @!visibility private
32+
FREE = []
33+
LOCK = Mutex.new
34+
ARRAYS = {} # used as a hash set
35+
@@next = 0
36+
private_constant :FREE, :LOCK, :ARRAYS
37+
38+
# @!macro [attach] thread_local_var_method_initialize
39+
#
40+
# Creates a thread local variable.
41+
#
42+
# @param [Object] default the default value when otherwise unset
43+
def initialize(default = nil)
44+
@default = default
45+
allocate_storage
46+
end
47+
48+
# @!macro thread_local_var_method_get
49+
def value
50+
if array = get_threadlocal_array
51+
value = array[@index]
52+
if value.nil?
53+
@default
54+
elsif value.equal?(NIL_SENTINEL)
55+
nil
56+
else
57+
value
58+
end
59+
else
60+
@default
61+
end
62+
end
63+
64+
# @!macro thread_local_var_method_set
65+
def value=(value)
66+
me = Thread.current
67+
# We could keep the thread-local arrays in a hash, keyed by Thread
68+
# But why? That would require locking
69+
# Using Ruby's built-in thread-local storage is faster
70+
unless array = get_threadlocal_array(me)
71+
array = set_threadlocal_array([], me)
72+
LOCK.synchronize { ARRAYS[array.object_id] = array }
73+
ObjectSpace.define_finalizer(me, self.class.thread_finalizer(array))
74+
end
75+
array[@index] = (value.nil? ? NIL_SENTINEL : value)
76+
value
77+
end
78+
79+
# @!macro thread_local_var_method_bind
80+
def bind(value, &block)
81+
if block_given?
82+
old_value = self.value
83+
begin
84+
self.value = value
85+
yield
86+
ensure
87+
self.value = old_value
88+
end
89+
end
90+
end
91+
92+
protected
93+
94+
# @!visibility private
95+
def allocate_storage
96+
@index = LOCK.synchronize do
97+
FREE.pop || begin
98+
result = @@next
99+
@@next += 1
100+
result
101+
end
102+
end
103+
ObjectSpace.define_finalizer(self, self.class.threadlocal_finalizer(@index))
104+
end
105+
106+
# @!visibility private
107+
def self.threadlocal_finalizer(index)
108+
proc do
109+
LOCK.synchronize do
110+
FREE.push(index)
111+
# The cost of GC'ing a TLV is linear in the number of threads using TLVs
112+
# But that is natural! More threads means more storage is used per TLV
113+
# So naturally more CPU time is required to free more storage
114+
ARRAYS.each_value do |array|
115+
array[index] = nil
116+
end
117+
end
118+
end
119+
end
120+
121+
# @!visibility private
122+
def self.thread_finalizer(array)
123+
proc do
124+
LOCK.synchronize do
125+
# The thread which used this thread-local array is now gone
126+
# So don't hold onto a reference to the array (thus blocking GC)
127+
ARRAYS.delete(array.object_id)
128+
end
129+
end
130+
end
131+
132+
private
133+
134+
if Thread.instance_methods.include?(:thread_variable_get)
135+
136+
def get_threadlocal_array(thread = Thread.current)
137+
thread.thread_variable_get(:__threadlocal_array__)
138+
end
139+
140+
def set_threadlocal_array(array, thread = Thread.current)
141+
thread.thread_variable_set(:__threadlocal_array__, array)
142+
end
143+
144+
else
145+
146+
def get_threadlocal_array(thread = Thread.current)
147+
thread[:__threadlocal_array__]
148+
end
149+
150+
def set_threadlocal_array(array, thread = Thread.current)
151+
thread[:__threadlocal_array__] = array
152+
end
153+
end
154+
155+
# This exists only for use in testing
156+
# @!visibility private
157+
def value_for(thread)
158+
if array = get_threadlocal_array(thread)
159+
value = array[@index]
160+
if value.nil?
161+
@default
162+
elsif value.equal?(NIL_SENTINEL)
163+
nil
164+
else
165+
value
166+
end
167+
else
168+
@default
169+
end
170+
end
171+
end
172+
end

0 commit comments

Comments
 (0)