Skip to content

Commit b4e7e18

Browse files
committed
Merge pull request #376 from alexdowad/experimental_tls_implementation
Experimental thread-local variable implementation
2 parents afea68f + f073cac commit b4e7e18

File tree

5 files changed

+191
-400
lines changed

5 files changed

+191
-400
lines changed

examples/thread_local_memory_usage.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env ruby
2+
3+
$: << File.expand_path('../../lib', __FILE__)
4+
5+
$DEBUG_TLV = true
6+
require 'concurrent'
7+
require 'concurrent/atomic/thread_local_var'
8+
require 'benchmark'
9+
require 'thread'
10+
11+
include Concurrent
12+
13+
# if we hold on to vars, but threads die, space used for TLVs should be recovered
14+
15+
def test_thread_gc(vars)
16+
threads = 500.times.collect do
17+
Thread.new do
18+
vars.each do |var|
19+
var.value = 1
20+
end
21+
end
22+
end
23+
threads.each(&:join)
24+
end
25+
26+
puts "BEFORE THREAD GC TEST:"
27+
puts "Ruby heap pages: #{GC.stat[:heap_length]}, Other malloc'd bytes: #{GC.stat[:malloc_increase]}"
28+
29+
vars = 500.times.collect { ThreadLocalVar.new(0) }
30+
200.times do
31+
test_thread_gc(vars)
32+
GC.start
33+
end
34+
35+
puts "AFTER THREAD GC TEST:"
36+
puts "Ruby heap pages: #{GC.stat[:heap_length]}, Other malloc'd bytes: #{GC.stat[:malloc_increase]}"
37+
38+
# if we hold on to threads, but drop TLVs, space used should be reused by allocated TLVs
39+
40+
def tlv_gc_test_loop(queue)
41+
while true
42+
var = queue.pop
43+
return if var.nil?
44+
var.value = 1
45+
end
46+
end
47+
48+
def test_tlv_gc(queues)
49+
500.times do
50+
var = ThreadLocalVar.new(0)
51+
queues.each { |q| q << var }
52+
end
53+
end
54+
55+
puts
56+
puts "BEFORE TLV GC TEST:"
57+
puts "Ruby heap pages: #{GC.stat[:heap_length]}, Other malloc'd bytes: #{GC.stat[:malloc_increase]}"
58+
59+
queues = 500.times.collect { Queue.new }
60+
threads = queues.map do |queue|
61+
Thread.new do
62+
tlv_gc_test_loop(queue)
63+
end
64+
end
65+
66+
200.times do
67+
test_tlv_gc(queues)
68+
GC.start
69+
end
70+
queues.each { |q| q << nil }
71+
threads.each(&:join)
72+
73+
puts "AFTER TLV GC TEST:"
74+
puts "Ruby heap pages: #{GC.stat[:heap_length]}, Other malloc'd bytes: #{GC.stat[:malloc_increase]}"

examples/thread_local_var_bench.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env ruby
2+
3+
$: << File.expand_path('../../lib', __FILE__)
4+
5+
require 'concurrent'
6+
require 'concurrent/atomic/thread_local_var'
7+
require 'benchmark'
8+
9+
include Concurrent
10+
11+
N_THREADS = 100
12+
N_VARS = 100
13+
14+
vars = N_VARS.times.collect { ThreadLocalVar.new(0) }
15+
16+
def test_threadlocal_perf(vars)
17+
threads = N_THREADS.times.collect do
18+
Thread.new do
19+
10000.times do
20+
index = rand(N_VARS)
21+
var = vars[index]
22+
var.value = var.value + 1
23+
end
24+
end
25+
end
26+
threads.each(&:join)
27+
end
28+
29+
Benchmark.bmbm do |bm|
30+
bm.report('ThreadLocalVar') { test_threadlocal_perf(vars) }
31+
end
Lines changed: 86 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
require 'concurrent/atomic/thread_local_var/weak_key_map'
1+
require 'thread'
22

33
module Concurrent
44

@@ -33,169 +33,132 @@ module Concurrent
3333
#
3434
# @see https://docs.oracle.com/javase/7/docs/api/java/lang/ThreadLocal.html Java ThreadLocal
3535
#
36-
# @!visibility private
37-
class AbstractThreadLocalVar
36+
class ThreadLocalVar
37+
38+
# Each thread has a (lazily initialized) array of thread-local variable values
39+
# Each time a new thread-local var is created, we allocate an "index" for it
40+
# For example, if the allocated index is 1, that means slot #1 in EVERY
41+
# thread's thread-local array will be used for the value of that TLV
42+
#
43+
# The good thing about using a per-THREAD structure to hold values, rather
44+
# than a per-TLV structure, is that no synchronization is needed when
45+
# reading and writing those values (since the structure is only ever
46+
# accessed by a single thread)
47+
#
48+
# Of course, when a TLV is GC'd, 1) we need to recover its index for use
49+
# by other new TLVs (otherwise the thread-local arrays could get bigger
50+
# and bigger with time), and 2) we need to null out all the references
51+
# held in the now-unused slots (both to avoid blocking GC of those objects,
52+
# and also to prevent "stale" values from being passed on to a new TLV
53+
# when the index is reused)
54+
# Because we need to null out freed slots, we need to keep references to
55+
# ALL the thread-local arrays -- ARRAYS is for that
56+
# But when a Thread is GC'd, we need to drop the reference to its thread-local
57+
# array, so we don't leak memory
3858

3959
# @!visibility private
4060
NIL_SENTINEL = Object.new
41-
private_constant :NIL_SENTINEL
61+
FREE = []
62+
LOCK = Mutex.new
63+
ARRAYS = {} # used as a hash set
64+
@@next = 0
65+
private_constant :NIL_SENTINEL, :FREE, :LOCK, :ARRAYS
4266

43-
# @!macro [attach] thread_local_var_method_initialize
44-
#
4567
# Creates a thread local variable.
4668
#
4769
# @param [Object] default the default value when otherwise unset
4870
def initialize(default = nil)
4971
@default = default
50-
allocate_storage
72+
@index = LOCK.synchronize do
73+
FREE.pop || begin
74+
result = @@next
75+
@@next += 1
76+
result
77+
end
78+
end
79+
ObjectSpace.define_finalizer(self, self.class.threadlocal_finalizer(@index))
5180
end
5281

53-
# @!macro [attach] thread_local_var_method_get
54-
#
5582
# Returns the value in the current thread's copy of this thread-local variable.
5683
#
5784
# @return [Object] the current value
5885
def value
59-
value = get
60-
61-
if value.nil?
62-
@default
63-
elsif value == NIL_SENTINEL
64-
nil
86+
if array = Thread.current[:__threadlocal_array__]
87+
value = array[@index]
88+
if value.nil?
89+
@default
90+
elsif value.equal?(NIL_SENTINEL)
91+
nil
92+
else
93+
value
94+
end
6595
else
66-
value
96+
@default
6797
end
6898
end
6999

70-
# @!macro [attach] thread_local_var_method_set
71-
#
72100
# Sets the current thread's copy of this thread-local variable to the specified value.
73-
#
101+
#
74102
# @param [Object] value the value to set
75103
# @return [Object] the new value
76104
def value=(value)
77-
bind value
78-
end
79-
80-
# @!macro [attach] thread_local_var_method_bind
81-
#
82-
# Bind the given value to thread local storage during
83-
# execution of the given block.
84-
#
85-
# @param [Object] value the value to bind
86-
# @yield the operation to be performed with the bound variable
87-
# @return [Object] the value
88-
def bind(value, &block)
89-
if value.nil?
90-
stored_value = NIL_SENTINEL
91-
else
92-
stored_value = value
105+
me = Thread.current
106+
# We could keep the thread-local arrays in a hash, keyed by Thread
107+
# But why? That would require locking
108+
# Using Ruby's built-in thread-local storage is faster
109+
unless array = me[:__threadlocal_array__]
110+
array = me[:__threadlocal_array__] = []
111+
LOCK.synchronize { ARRAYS[array.object_id] = array }
112+
ObjectSpace.define_finalizer(me, self.class.thread_finalizer(array))
93113
end
94-
95-
set(stored_value, &block)
96-
114+
array[@index] = (value.nil? ? NIL_SENTINEL : value)
97115
value
98116
end
99117

100118
protected
101119

102120
# @!visibility private
103-
def allocate_storage
104-
raise NotImplementedError
105-
end
106-
107-
# @!visibility private
108-
def get
109-
raise NotImplementedError
110-
end
111-
112-
# @!visibility private
113-
def set(value)
114-
raise NotImplementedError
115-
end
116-
end
117-
118-
# @!visibility private
119-
# @!macro internal_implementation_note
120-
class RubyThreadLocalVar < AbstractThreadLocalVar
121-
122-
protected
123-
124-
# @!visibility private
125-
def allocate_storage
126-
@storage = WeakKeyMap.new
121+
def self.threadlocal_finalizer(index)
122+
proc do
123+
LOCK.synchronize do
124+
FREE.push(index)
125+
# The cost of GC'ing a TLV is linear in the number of threads using TLVs
126+
# But that is natural! More threads means more storage is used per TLV
127+
# So naturally more CPU time is required to free more storage
128+
ARRAYS.each_value do |array|
129+
array[index] = nil
130+
end
131+
end
132+
end
127133
end
128134

129135
# @!visibility private
130-
def get
131-
@storage[Thread.current]
136+
def self.thread_finalizer(array)
137+
proc do
138+
LOCK.synchronize do
139+
# The thread which used this thread-local array is now gone
140+
# So don't hold onto a reference to the array (thus blocking GC)
141+
ARRAYS.delete(array.object_id)
142+
end
143+
end
132144
end
133145

134-
# @!visibility private
135-
def set(value)
136-
key = Thread.current
137-
138-
@storage[key] = value
139-
146+
# Bind the given value to thread local storage during
147+
# execution of the given block.
148+
#
149+
# @param [Object] value the value to bind
150+
# @yield the operation to be performed with the bound variable
151+
# @return [Object] the value
152+
def bind(value, &block)
140153
if block_given?
154+
old_value = self.value
141155
begin
156+
self.value = value
142157
yield
143158
ensure
144-
@storage.delete(key)
159+
self.value = old_value
145160
end
146161
end
147162
end
148163
end
149-
150-
if Concurrent.on_jruby?
151-
152-
# @!visibility private
153-
# @!macro internal_implementation_note
154-
class JavaThreadLocalVar < AbstractThreadLocalVar
155-
156-
protected
157-
158-
# @!visibility private
159-
def allocate_storage
160-
@var = java.lang.ThreadLocal.new
161-
end
162-
163-
# @!visibility private
164-
def get
165-
@var.get
166-
end
167-
168-
# @!visibility private
169-
def set(value)
170-
@var.set(value)
171-
end
172-
end
173-
end
174-
175-
# @!visibility private
176-
# @!macro internal_implementation_note
177-
ThreadLocalVarImplementation = case
178-
when Concurrent.on_jruby?
179-
JavaThreadLocalVar
180-
else
181-
RubyThreadLocalVar
182-
end
183-
private_constant :ThreadLocalVarImplementation
184-
185-
# @!macro thread_local_var
186-
class ThreadLocalVar < ThreadLocalVarImplementation
187-
188-
# @!method initialize(default = nil)
189-
# @!macro thread_local_var_method_initialize
190-
191-
# @!method value
192-
# @!macro thread_local_var_method_get
193-
194-
# @!method value=(value)
195-
# @!macro thread_local_var_method_set
196-
197-
# @!method bind(value, &block)
198-
# @!macro thread_local_var_method_bind
199-
200-
end
201164
end

0 commit comments

Comments
 (0)