Skip to content

Commit 8f15a3e

Browse files
committed
Merge pull request #164 from ruby-concurrency/thread-local-var
Fix Thread leaking in ThreadLocalVariables Tested the exclusion of the `Ref` gem in the pre-compiled Java build (using our automated build process) and it worked as intended. The `Ref` gem will not be installed under JRuby when using the precompiled Java build.
2 parents 9513562 + 1159d5a commit 8f15a3e

File tree

4 files changed

+84
-34
lines changed

4 files changed

+84
-34
lines changed

concurrent-ruby.gemspec

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ Gem::Specification.new do |s|
3131
end
3232

3333
s.required_ruby_version = '>= 1.9.3'
34+
35+
unless defined?(JRUBY_VERSION)
36+
s.add_dependency 'ref', '~> 1.0.5'
37+
end
3438
end

lib/concurrent/atomic/thread_local_var.rb

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,56 @@
22

33
module Concurrent
44

5-
module ThreadLocalRubyStorage
5+
class AbstractThreadLocalVar
66

7-
def allocate_storage
8-
@storage = Atomic.new Hash.new
9-
end
7+
module ThreadLocalRubyStorage
108

11-
def get
12-
@storage.get[Thread.current]
13-
end
9+
protected
1410

15-
def set(value)
16-
@storage.update { |s| s.merge Thread.current => value }
17-
end
11+
unless RUBY_PLATFORM == 'java'
12+
require 'ref'
13+
end
1814

19-
end
15+
def allocate_storage
16+
@storage = Ref::WeakKeyMap.new
17+
end
2018

21-
module ThreadLocalJavaStorage
19+
def get
20+
@storage[Thread.current]
21+
end
2222

23-
protected
23+
def set(value, &block)
24+
key = Thread.current
2425

25-
def allocate_storage
26-
@var = java.lang.ThreadLocal.new
27-
end
26+
@storage[key] = value
2827

29-
def get
30-
@var.get
28+
if block_given?
29+
begin
30+
block.call
31+
ensure
32+
@storage.delete key
33+
end
34+
end
35+
end
3136
end
3237

33-
def set(value)
34-
@var.set(value)
35-
end
38+
module ThreadLocalJavaStorage
3639

37-
end
40+
protected
3841

39-
class AbstractThreadLocalVar
42+
def allocate_storage
43+
@var = java.lang.ThreadLocal.new
44+
end
45+
46+
def get
47+
@var.get
48+
end
49+
50+
def set(value)
51+
@var.set(value)
52+
end
53+
54+
end
4055

4156
NIL_SENTINEL = Object.new
4257

@@ -58,13 +73,17 @@ def value
5873
end
5974

6075
def value=(value)
76+
bind value
77+
end
78+
79+
def bind(value, &block)
6180
if value.nil?
6281
stored_value = NIL_SENTINEL
6382
else
6483
stored_value = value
6584
end
6685

67-
set stored_value
86+
set stored_value, &block
6887

6988
value
7089
end

lib/concurrent/atomics.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,4 @@
77
require 'concurrent/atomic/cyclic_barrier'
88
require 'concurrent/atomic/count_down_latch'
99
require 'concurrent/atomic/event'
10-
require 'concurrent/atomic/thread_local_var'
1110
require 'concurrent/atomic/synchronization'

spec/concurrent/atomic/thread_local_var_spec.rb

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
module Concurrent
55

6+
require 'concurrent/atomic/thread_local_var'
7+
68
describe ThreadLocalVar do
79

8-
subject{ ThreadLocalVar.new }
10+
subject { ThreadLocalVar.new }
911

1012
context '#initialize' do
1113

@@ -20,7 +22,7 @@ module Concurrent
2022
end
2123

2224
it 'sets the same initial value for all threads' do
23-
v = ThreadLocalVar.new(14)
25+
v = ThreadLocalVar.new(14)
2426
t1 = Thread.new { v.value }
2527
t2 = Thread.new { v.value }
2628
expect(t1.value).to eq 14
@@ -29,11 +31,37 @@ module Concurrent
2931

3032
if jruby?
3133
it 'uses ThreadLocalJavaStorage' do
32-
expect(subject.class.ancestors).to include(Concurrent::ThreadLocalJavaStorage)
34+
expect(subject.class.ancestors).to include(Concurrent::AbstractThreadLocalVar::ThreadLocalJavaStorage)
3335
end
3436
else
3537
it 'uses ThreadLocalNewStorage' do
36-
expect(subject.class.ancestors).to include(Concurrent::ThreadLocalRubyStorage)
38+
expect(subject.class.ancestors).to include(Concurrent::AbstractThreadLocalVar::ThreadLocalRubyStorage)
39+
end
40+
end
41+
end
42+
43+
unless jruby?
44+
context 'GC' do
45+
it 'does not leave values behind when bind is used' do
46+
var = ThreadLocalVar.new(0)
47+
100.times.map do |i|
48+
Thread.new { var.bind(i) { var.value } }
49+
end.each(&:join)
50+
var.value = 0
51+
expect(var.instance_variable_get(:@storage).keys.size).to be == 1
52+
end
53+
54+
it 'does not leave values behind when bind is not used' do
55+
var = ThreadLocalVar.new(0)
56+
100.times.map do |i|
57+
Thread.new { var.value = i; var.value }
58+
end.each(&:join)
59+
var.value = 0
60+
sleep 0.1
61+
GC.start
62+
sleep 0.1
63+
GC.start
64+
expect(var.instance_variable_get(:@storage).keys.size).to be == 1
3765
end
3866
end
3967
end
@@ -46,7 +74,7 @@ module Concurrent
4674
end
4775

4876
it 'returns the value after modification' do
49-
v = ThreadLocalVar.new(14)
77+
v = ThreadLocalVar.new(14)
5078
v.value = 2
5179
expect(v.value).to eq 2
5280
end
@@ -56,7 +84,7 @@ module Concurrent
5684
context '#value=' do
5785

5886
it 'sets a new value' do
59-
v = ThreadLocalVar.new(14)
87+
v = ThreadLocalVar.new(14)
6088
v.value = 2
6189
expect(v.value).to eq 2
6290
end
@@ -67,14 +95,14 @@ module Concurrent
6795
end
6896

6997
it 'does not modify the initial value for other threads' do
70-
v = ThreadLocalVar.new(14)
98+
v = ThreadLocalVar.new(14)
7199
v.value = 2
72-
t = Thread.new { v.value }
100+
t = Thread.new { v.value }
73101
expect(t.value).to eq 14
74102
end
75103

76104
it 'does not modify the value for other threads' do
77-
v = ThreadLocalVar.new(14)
105+
v = ThreadLocalVar.new(14)
78106
v.value = 2
79107

80108
b1 = CountDownLatch.new(2)

0 commit comments

Comments
 (0)