Skip to content

Commit 0bd6a48

Browse files
committed
Merge pull request #387 from ruby-concurrency/more-thread-local-var
Updates to ThreadLocalVar
2 parents f6b35d9 + 8af1166 commit 0bd6a48

File tree

2 files changed

+154
-59
lines changed

2 files changed

+154
-59
lines changed

lib/concurrent/atomic/thread_local_var.rb

Lines changed: 139 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,65 @@ module Concurrent
3333
#
3434
# @see https://docs.oracle.com/javase/7/docs/api/java/lang/ThreadLocal.html Java ThreadLocal
3535
#
36-
class ThreadLocalVar
36+
# @!visibility private
37+
class AbstractThreadLocalVar
38+
39+
# @!visibility private
40+
NIL_SENTINEL = Object.new
41+
private_constant :NIL_SENTINEL
42+
43+
# @!macro [attach] thread_local_var_method_initialize
44+
#
45+
# Creates a thread local variable.
46+
#
47+
# @param [Object] default the default value when otherwise unset
48+
def initialize(default = nil)
49+
@default = default
50+
allocate_storage
51+
end
52+
53+
# @!macro [attach] thread_local_var_method_get
54+
#
55+
# Returns the value in the current thread's copy of this thread-local variable.
56+
#
57+
# @return [Object] the current value
58+
def value
59+
raise NotImplementedError
60+
end
61+
62+
# @!macro [attach] thread_local_var_method_set
63+
#
64+
# Sets the current thread's copy of this thread-local variable to the specified value.
65+
#
66+
# @param [Object] value the value to set
67+
# @return [Object] the new value
68+
def value=(value)
69+
raise NotImplementedError
70+
end
71+
72+
# @!macro [attach] thread_local_var_method_bind
73+
#
74+
# Bind the given value to thread local storage during
75+
# execution of the given block.
76+
#
77+
# @param [Object] value the value to bind
78+
# @yield the operation to be performed with the bound variable
79+
# @return [Object] the value
80+
def bind(value, &block)
81+
raise NotImplementedError
82+
end
83+
84+
protected
85+
86+
# @!visibility private
87+
def allocate_storage
88+
raise NotImplementedError
89+
end
90+
end
91+
92+
# @!visibility private
93+
# @!macro internal_implementation_note
94+
class RubyThreadLocalVar < AbstractThreadLocalVar
3795

3896
# Each thread has a (lazily initialized) array of thread-local variable values
3997
# Each time a new thread-local var is created, we allocate an "index" for it
@@ -57,33 +115,25 @@ class ThreadLocalVar
57115
# array, so we don't leak memory
58116

59117
# @!visibility private
60-
NIL_SENTINEL = Object.new
61118
FREE = []
62119
LOCK = Mutex.new
63120
ARRAYS = {} # used as a hash set
64121
@@next = 0
65-
private_constant :NIL_SENTINEL, :FREE, :LOCK, :ARRAYS
122+
private_constant :FREE, :LOCK, :ARRAYS
66123

124+
# @!macro [attach] thread_local_var_method_initialize
125+
#
67126
# Creates a thread local variable.
68127
#
69128
# @param [Object] default the default value when otherwise unset
70129
def initialize(default = nil)
71130
@default = default
72-
@index = LOCK.synchronize do
73-
FREE.pop || begin
74-
result = @@next
75-
@@next += 1
76-
result
77-
end
78-
end
79-
ObjectSpace.define_finalizer(self, self.class.threadlocal_finalizer(@index))
131+
allocate_storage
80132
end
81133

82-
# Returns the value in the current thread's copy of this thread-local variable.
83-
#
84-
# @return [Object] the current value
134+
# @!macro thread_local_var_method_get
85135
def value
86-
if array = Thread.current[:__threadlocal_array__]
136+
if array = Thread.current.thread_variable_get(:__threadlocal_array__)
87137
value = array[@index]
88138
if value.nil?
89139
@default
@@ -97,30 +147,22 @@ def value
97147
end
98148
end
99149

100-
# Sets the current thread's copy of this thread-local variable to the specified value.
101-
#
102-
# @param [Object] value the value to set
103-
# @return [Object] the new value
150+
# @!macro thread_local_var_method_set
104151
def value=(value)
105152
me = Thread.current
106153
# We could keep the thread-local arrays in a hash, keyed by Thread
107154
# But why? That would require locking
108155
# Using Ruby's built-in thread-local storage is faster
109-
unless array = me[:__threadlocal_array__]
110-
array = me[:__threadlocal_array__] = []
156+
unless array = me.thread_variable_get(:__threadlocal_array__)
157+
array = me.thread_variable_set(:__threadlocal_array__, [])
111158
LOCK.synchronize { ARRAYS[array.object_id] = array }
112159
ObjectSpace.define_finalizer(me, self.class.thread_finalizer(array))
113160
end
114161
array[@index] = (value.nil? ? NIL_SENTINEL : value)
115162
value
116163
end
117164

118-
# Bind the given value to thread local storage during
119-
# execution of the given block.
120-
#
121-
# @param [Object] value the value to bind
122-
# @yield the operation to be performed with the bound variable
123-
# @return [Object] the value
165+
# @!macro thread_local_var_method_bind
124166
def bind(value, &block)
125167
if block_given?
126168
old_value = self.value
@@ -135,6 +177,18 @@ def bind(value, &block)
135177

136178
protected
137179

180+
# @!visibility private
181+
def allocate_storage
182+
@index = LOCK.synchronize do
183+
FREE.pop || begin
184+
result = @@next
185+
@@next += 1
186+
result
187+
end
188+
end
189+
ObjectSpace.define_finalizer(self, self.class.threadlocal_finalizer(@index))
190+
end
191+
138192
# @!visibility private
139193
def self.threadlocal_finalizer(index)
140194
proc do
@@ -160,43 +214,78 @@ def self.thread_finalizer(array)
160214
end
161215
end
162216
end
217+
end
163218

164-
private
219+
if Concurrent.on_jruby?
165220

166-
# This exists only for use in testing
167221
# @!visibility private
168-
def value_for(thread)
169-
if array = thread[:__threadlocal_array__]
170-
value = array[@index]
222+
# @!macro internal_implementation_note
223+
class JavaThreadLocalVar < AbstractThreadLocalVar
224+
225+
# @!macro thread_local_var_method_get
226+
def value
227+
value = @var.get
228+
171229
if value.nil?
172230
@default
173-
elsif value.equal?(NIL_SENTINEL)
231+
elsif value == NIL_SENTINEL
174232
nil
175233
else
176234
value
177235
end
178-
else
179-
@default
180236
end
181-
end
182237

183-
private
238+
# @!macro thread_local_var_method_set
239+
def value=(value)
240+
@var.set(value)
241+
end
184242

185-
# This exists only for use in testing
186-
# @!visibility private
187-
def value_for(thread)
188-
if array = thread[:__threadlocal_array__]
189-
value = array[@index]
190-
if value.nil?
191-
@default
192-
elsif value.equal?(NIL_SENTINEL)
193-
nil
194-
else
195-
value
243+
# @!macro thread_local_var_method_bind
244+
def bind(value, &block)
245+
if block_given?
246+
old_value = @var.get
247+
begin
248+
@var.set(value)
249+
yield
250+
ensure
251+
@var.set(old_value)
252+
end
196253
end
197-
else
198-
@default
254+
end
255+
256+
protected
257+
258+
# @!visibility private
259+
def allocate_storage
260+
@var = java.lang.ThreadLocal.new
199261
end
200262
end
201263
end
264+
265+
# @!visibility private
266+
# @!macro internal_implementation_note
267+
ThreadLocalVarImplementation = case
268+
when Concurrent.on_jruby?
269+
JavaThreadLocalVar
270+
else
271+
RubyThreadLocalVar
272+
end
273+
private_constant :ThreadLocalVarImplementation
274+
275+
# @!macro thread_local_var
276+
class ThreadLocalVar < ThreadLocalVarImplementation
277+
278+
# @!method initialize(default = nil)
279+
# @!macro thread_local_var_method_initialize
280+
281+
# @!method value
282+
# @!macro thread_local_var_method_get
283+
284+
# @!method value=(value)
285+
# @!macro thread_local_var_method_set
286+
287+
# @!method bind(value, &block)
288+
# @!macro thread_local_var_method_bind
289+
290+
end
202291
end

spec/concurrent/atomic/thread_local_var_spec.rb

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ module Concurrent
2727
expect(t1.value).to eq 14
2828
expect(t2.value).to eq 14
2929
end
30+
31+
if Concurrent.on_jruby?
32+
it 'extends JavaThreadLocalVar' do
33+
expect(subject.class.ancestors).to include(Concurrent::JavaThreadLocalVar)
34+
end
35+
else
36+
it 'extends RubyThreadLocalVar' do
37+
expect(subject.class.ancestors).to include(Concurrent::RubyThreadLocalVar)
38+
end
39+
end
3040
end
3141

3242
context '#value' do
@@ -37,17 +47,16 @@ module Concurrent
3747
end
3848

3949
it 'returns the value after modification' do
40-
v = ThreadLocalVar.new(14)
50+
v = ThreadLocalVar.new(14)
4151
v.value = 2
4252
expect(v.value).to eq 2
4353
end
44-
4554
end
4655

4756
context '#value=' do
4857

4958
it 'sets a new value' do
50-
v = ThreadLocalVar.new(14)
59+
v = ThreadLocalVar.new(14)
5160
v.value = 2
5261
expect(v.value).to eq 2
5362
end
@@ -58,14 +67,14 @@ module Concurrent
5867
end
5968

6069
it 'does not modify the initial value for other threads' do
61-
v = ThreadLocalVar.new(14)
70+
v = ThreadLocalVar.new(14)
6271
v.value = 2
63-
t = Thread.new { v.value }
72+
t = Thread.new { v.value }
6473
expect(t.value).to eq 14
6574
end
6675

6776
it 'does not modify the value for other threads' do
68-
v = ThreadLocalVar.new(14)
77+
v = ThreadLocalVar.new(14)
6978
v.value = 2
7079

7180
b1 = CountDownLatch.new(2)
@@ -92,9 +101,6 @@ module Concurrent
92101
expect(t1.value).to eq 1
93102
expect(t2.value).to eq 2
94103
end
95-
96104
end
97-
98105
end
99-
100106
end

0 commit comments

Comments
 (0)