Skip to content

Commit 97fa0fb

Browse files
authored
Merge pull request #821 from ruby-concurrency/segfault
Safer finalizers
2 parents 226aeec + 4d04a24 commit 97fa0fb

File tree

1 file changed

+43
-33
lines changed

1 file changed

+43
-33
lines changed

lib/concurrent/atomic/ruby_thread_local_var.rb

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,38 @@ class RubyThreadLocalVar < AbstractThreadLocalVar
3232
FREE = []
3333
LOCK = Mutex.new
3434
ARRAYS = {} # used as a hash set
35+
# noinspection RubyClassVariableUsageInspection
3536
@@next = 0
36-
private_constant :FREE, :LOCK, :ARRAYS
37+
QUEUE = Queue.new
38+
THREAD = Thread.new do
39+
while true
40+
method, i = QUEUE.pop
41+
case method
42+
when :thread_local_finalizer
43+
LOCK.synchronize do
44+
FREE.push(i)
45+
# The cost of GC'ing a TLV is linear in the number of threads using TLVs
46+
# But that is natural! More threads means more storage is used per TLV
47+
# So naturally more CPU time is required to free more storage
48+
ARRAYS.each_value do |array|
49+
array[i] = nil
50+
end
51+
end
52+
when :thread_finalizer
53+
LOCK.synchronize do
54+
# The thread which used this thread-local array is now gone
55+
# So don't hold onto a reference to the array (thus blocking GC)
56+
ARRAYS.delete(i)
57+
end
58+
end
59+
end
60+
end
61+
62+
private_constant :FREE, :LOCK, :ARRAYS, :QUEUE, :THREAD
3763

3864
# @!macro thread_local_var_method_get
3965
def value
40-
if array = get_threadlocal_array
66+
if (array = get_threadlocal_array)
4167
value = array[@index]
4268
if value.nil?
4369
default
@@ -57,10 +83,10 @@ def value=(value)
5783
# We could keep the thread-local arrays in a hash, keyed by Thread
5884
# But why? That would require locking
5985
# Using Ruby's built-in thread-local storage is faster
60-
unless array = get_threadlocal_array(me)
86+
unless (array = get_threadlocal_array(me))
6187
array = set_threadlocal_array([], me)
6288
LOCK.synchronize { ARRAYS[array.object_id] = array }
63-
ObjectSpace.define_finalizer(me, self.class.thread_finalizer(array))
89+
ObjectSpace.define_finalizer(me, self.class.thread_finalizer(array.object_id))
6490
end
6591
array[@index] = (value.nil? ? NULL : value)
6692
value
@@ -69,6 +95,7 @@ def value=(value)
6995
protected
7096

7197
# @!visibility private
98+
# noinspection RubyClassVariableUsageInspection
7299
def allocate_storage
73100
@index = LOCK.synchronize do
74101
FREE.pop || begin
@@ -77,37 +104,19 @@ def allocate_storage
77104
result
78105
end
79106
end
80-
ObjectSpace.define_finalizer(self, self.class.threadlocal_finalizer(@index))
107+
ObjectSpace.define_finalizer(self, self.class.thread_local_finalizer(@index))
81108
end
82109

83110
# @!visibility private
84-
def self.threadlocal_finalizer(index)
85-
proc do
86-
Thread.new do # avoid error: can't be called from trap context
87-
LOCK.synchronize do
88-
FREE.push(index)
89-
# The cost of GC'ing a TLV is linear in the number of threads using TLVs
90-
# But that is natural! More threads means more storage is used per TLV
91-
# So naturally more CPU time is required to free more storage
92-
ARRAYS.each_value do |array|
93-
array[index] = nil
94-
end
95-
end
96-
end
97-
end
111+
def self.thread_local_finalizer(index)
112+
# avoid error: can't be called from trap context
113+
proc { QUEUE.push [:thread_local_finalizer, index] }
98114
end
99115

100116
# @!visibility private
101-
def self.thread_finalizer(array)
102-
proc do
103-
Thread.new do # avoid error: can't be called from trap context
104-
LOCK.synchronize do
105-
# The thread which used this thread-local array is now gone
106-
# So don't hold onto a reference to the array (thus blocking GC)
107-
ARRAYS.delete(array.object_id)
108-
end
109-
end
110-
end
117+
def self.thread_finalizer(id)
118+
# avoid error: can't be called from trap context
119+
proc { QUEUE.push [:thread_finalizer, id] }
111120
end
112121

113122
private
@@ -136,21 +145,22 @@ def set_threadlocal_array(array, thread = Thread.current)
136145
# This exists only for use in testing
137146
# @!visibility private
138147
def value_for(thread)
139-
if array = get_threadlocal_array(thread)
148+
if (array = get_threadlocal_array(thread))
140149
value = array[@index]
141150
if value.nil?
142-
default_for(thread)
151+
get_default
143152
elsif value.equal?(NULL)
144153
nil
145154
else
146155
value
147156
end
148157
else
149-
default_for(thread)
158+
get_default
150159
end
151160
end
152161

153-
def default_for(thread)
162+
# @!visibility private
163+
def get_default
154164
if @default_block
155165
raise "Cannot use default_for with default block"
156166
else

0 commit comments

Comments
 (0)