Skip to content

Commit 4be7c50

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

File tree

2 files changed

+58
-11
lines changed

2 files changed

+58
-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: 20 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,17 @@ module Concurrent
3840
end
3941
end
4042

43+
context 'GC' do
44+
it 'does not leave values behind when bind is used' do
45+
var = ThreadLocalVar.new(0)
46+
100.times.map do |i|
47+
Thread.new { var.bind(i) { var.value } }
48+
end.each(&:join)
49+
var.value = 0
50+
expect(var.instance_variable_get(:@storage).get.size).to be == 1
51+
end
52+
end
53+
4154
context '#value' do
4255

4356
it 'returns the current value' do
@@ -46,7 +59,7 @@ module Concurrent
4659
end
4760

4861
it 'returns the value after modification' do
49-
v = ThreadLocalVar.new(14)
62+
v = ThreadLocalVar.new(14)
5063
v.value = 2
5164
expect(v.value).to eq 2
5265
end
@@ -56,7 +69,7 @@ module Concurrent
5669
context '#value=' do
5770

5871
it 'sets a new value' do
59-
v = ThreadLocalVar.new(14)
72+
v = ThreadLocalVar.new(14)
6073
v.value = 2
6174
expect(v.value).to eq 2
6275
end
@@ -67,14 +80,14 @@ module Concurrent
6780
end
6881

6982
it 'does not modify the initial value for other threads' do
70-
v = ThreadLocalVar.new(14)
83+
v = ThreadLocalVar.new(14)
7184
v.value = 2
72-
t = Thread.new { v.value }
85+
t = Thread.new { v.value }
7386
expect(t.value).to eq 14
7487
end
7588

7689
it 'does not modify the value for other threads' do
77-
v = ThreadLocalVar.new(14)
90+
v = ThreadLocalVar.new(14)
7891
v.value = 2
7992

8093
b1 = CountDownLatch.new(2)

0 commit comments

Comments
 (0)