Skip to content

Commit da60120

Browse files
committed
Fix Thread leaking in ThreadLocalVariables
- introduce #bind method to avoid value leaks - warn on using just #value=
1 parent cfd030f commit da60120

File tree

2 files changed

+60
-11
lines changed

2 files changed

+60
-11
lines changed

lib/concurrent/atomic/thread_local_var.rb

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,45 @@ module Concurrent
44

55
module ThreadLocalRubyStorage
66

7+
protected
8+
79
def allocate_storage
810
@storage = Atomic.new Hash.new
911
end
1012

1113
def get
12-
@storage.get[Thread.current]
14+
@storage.get[Thread.current.object_id]
1315
end
1416

15-
def set(value)
16-
@storage.update { |s| s.merge Thread.current => value }
17+
def set(value, &block)
18+
key = Thread.current.object_id
19+
20+
@storage.update do |s|
21+
s.merge(key => value)
22+
end
23+
24+
if block_given?
25+
begin
26+
block.call
27+
ensure
28+
@storage.update do |s|
29+
s.clone.tap { |h| h.delete key }
30+
end
31+
end
32+
33+
else
34+
unless ThreadLocalRubyStorage.i_know_it_may_leak_values?
35+
warn "it may leak values if used without block\n#{caller[0]}"
36+
end
37+
end
38+
end
39+
40+
def self.i_know_it_may_leak_values!
41+
@leak_acknowledged = true
42+
end
43+
44+
def self.i_know_it_may_leak_values?
45+
@leak_acknowledged
1746
end
1847

1948
end
@@ -57,14 +86,19 @@ def value
5786
end
5887
end
5988

89+
# may leak the value, #bind is preferred
6090
def value=(value)
91+
bind value
92+
end
93+
94+
def bind(value, &block)
6195
if value.nil?
6296
stored_value = NIL_SENTINEL
6397
else
6498
stored_value = value
6599
end
66100

67-
set stored_value
101+
set stored_value, &block
68102

69103
value
70104
end

spec/concurrent/atomic/thread_local_var_spec.rb

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

44
module Concurrent
55

6+
ThreadLocalRubyStorage.i_know_it_may_leak_values!
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
@@ -38,6 +40,19 @@ module Concurrent
3840
end
3941
end
4042

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).get.size).to be == 1
52+
end
53+
end
54+
end
55+
4156
context '#value' do
4257

4358
it 'returns the current value' do
@@ -46,7 +61,7 @@ module Concurrent
4661
end
4762

4863
it 'returns the value after modification' do
49-
v = ThreadLocalVar.new(14)
64+
v = ThreadLocalVar.new(14)
5065
v.value = 2
5166
expect(v.value).to eq 2
5267
end
@@ -56,7 +71,7 @@ module Concurrent
5671
context '#value=' do
5772

5873
it 'sets a new value' do
59-
v = ThreadLocalVar.new(14)
74+
v = ThreadLocalVar.new(14)
6075
v.value = 2
6176
expect(v.value).to eq 2
6277
end
@@ -67,14 +82,14 @@ module Concurrent
6782
end
6883

6984
it 'does not modify the initial value for other threads' do
70-
v = ThreadLocalVar.new(14)
85+
v = ThreadLocalVar.new(14)
7186
v.value = 2
72-
t = Thread.new { v.value }
87+
t = Thread.new { v.value }
7388
expect(t.value).to eq 14
7489
end
7590

7691
it 'does not modify the value for other threads' do
77-
v = ThreadLocalVar.new(14)
92+
v = ThreadLocalVar.new(14)
7893
v.value = 2
7994

8095
b1 = CountDownLatch.new(2)

0 commit comments

Comments
 (0)