Skip to content

Commit acb8d70

Browse files
authored
Merge pull request #856 from ruby-concurrency/segfault
RubyThreadLocalVar: rely on GIL on MRI to avoid problems with thread/mutex/queue in finalizers
2 parents 7dc6eb0 + a6654b3 commit acb8d70

File tree

1 file changed

+49
-42
lines changed

1 file changed

+49
-42
lines changed

lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -28,38 +28,27 @@ class RubyThreadLocalVar < AbstractThreadLocalVar
2828
# But when a Thread is GC'd, we need to drop the reference to its thread-local
2929
# array, so we don't leak memory
3030

31-
# @!visibility private
32-
FREE = []
33-
LOCK = Mutex.new
34-
ARRAYS = {} # used as a hash set
35-
# noinspection RubyClassVariableUsageInspection
36-
@@next = 0
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
31+
FREE = []
32+
LOCK = Mutex.new
33+
THREAD_LOCAL_ARRAYS = {} # used as a hash set
34+
35+
# synchronize when not on MRI
36+
# on MRI using lock in finalizer leads to "can't be called from trap context" error
37+
# so the code is carefully written to be tread-safe on MRI relying on GIL
38+
39+
if Concurrent.on_cruby?
40+
# @!visibility private
41+
def self.semi_sync(&block)
42+
block.call
43+
end
44+
else
45+
# @!visibility private
46+
def self.semi_sync(&block)
47+
LOCK.synchronize(&block)
5948
end
6049
end
6150

62-
private_constant :FREE, :LOCK, :ARRAYS, :QUEUE, :THREAD
51+
private_constant :FREE, :LOCK, :THREAD_LOCAL_ARRAYS
6352

6453
# @!macro thread_local_var_method_get
6554
def value
@@ -85,7 +74,7 @@ def value=(value)
8574
# Using Ruby's built-in thread-local storage is faster
8675
unless (array = get_threadlocal_array(me))
8776
array = set_threadlocal_array([], me)
88-
LOCK.synchronize { ARRAYS[array.object_id] = array }
77+
self.class.semi_sync { THREAD_LOCAL_ARRAYS[array.object_id] = array }
8978
ObjectSpace.define_finalizer(me, self.class.thread_finalizer(array.object_id))
9079
end
9180
array[@index] = (value.nil? ? NULL : value)
@@ -95,32 +84,50 @@ def value=(value)
9584
protected
9685

9786
# @!visibility private
98-
# noinspection RubyClassVariableUsageInspection
9987
def allocate_storage
100-
@index = LOCK.synchronize do
101-
FREE.pop || begin
102-
result = @@next
103-
@@next += 1
104-
result
105-
end
106-
end
88+
@index = FREE.pop || next_index
89+
10790
ObjectSpace.define_finalizer(self, self.class.thread_local_finalizer(@index))
10891
end
10992

11093
# @!visibility private
11194
def self.thread_local_finalizer(index)
112-
# avoid error: can't be called from trap context
113-
proc { QUEUE.push [:thread_local_finalizer, index] }
95+
proc do
96+
semi_sync do
97+
# The cost of GC'ing a TLV is linear in the number of threads using TLVs
98+
# But that is natural! More threads means more storage is used per TLV
99+
# So naturally more CPU time is required to free more storage
100+
THREAD_LOCAL_ARRAYS.each_value { |array| array[index] = nil }
101+
# free index has to be published after the arrays are cleared
102+
FREE.push(index)
103+
end
104+
end
114105
end
115106

116107
# @!visibility private
117108
def self.thread_finalizer(id)
118-
# avoid error: can't be called from trap context
119-
proc { QUEUE.push [:thread_finalizer, id] }
109+
proc do
110+
semi_sync do
111+
# The thread which used this thread-local array is now gone
112+
# So don't hold onto a reference to the array (thus blocking GC)
113+
THREAD_LOCAL_ARRAYS.delete(id)
114+
end
115+
end
120116
end
121117

122118
private
123119

120+
# noinspection RubyClassVariableUsageInspection
121+
@@next = 0
122+
# noinspection RubyClassVariableUsageInspection
123+
def next_index
124+
LOCK.synchronize do
125+
result = @@next
126+
@@next += 1
127+
result
128+
end
129+
end
130+
124131
if Thread.instance_methods.include?(:thread_variable_get)
125132

126133
def get_threadlocal_array(thread = Thread.current)

0 commit comments

Comments
 (0)