Skip to content

Commit 759a1ef

Browse files
committed
Merge pull request #293 from ruby-concurrency/weak-key-map
Added WeakKeyMap to ThreadLocalRubyStorage.
2 parents 9dd296a + 6dadc20 commit 759a1ef

File tree

4 files changed

+242
-9
lines changed

4 files changed

+242
-9
lines changed

concurrent-ruby.gemspec

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,5 @@ Inspired by Erlang, Clojure, Go, JavaScript, actors, and classic concurrency pat
2828
s.platform = 'java'
2929
end
3030

31-
s.add_runtime_dependency 'ref', '~> 1.0', '>= 1.0.5'
32-
3331
s.required_ruby_version = '>= 1.9.3'
3432
end

lib/concurrent/atomic/thread_local_var.rb

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'concurrent/atomic/thread_local_var/weak_key_map'
2+
13
module Concurrent
24

35
# @!macro [attach] abstract_thread_local_var
@@ -33,12 +35,8 @@ module ThreadLocalRubyStorage
3335

3436
protected
3537

36-
unless Concurrent.on_jruby?
37-
require 'ref'
38-
end
39-
4038
def allocate_storage
41-
@storage = Ref::WeakKeyMap.new
39+
@storage = WeakKeyMap.new
4240
end
4341

4442
def get
@@ -123,5 +121,4 @@ class ThreadLocalVar < AbstractThreadLocalVar
123121
include ThreadLocalRubyStorage
124122
end
125123
end
126-
127124
end
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# Copyright (c) 2013 Brian Durand
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining
4+
# a copy of this software and associated documentation files (the
5+
# "Software"), to deal in the Software without restriction, including
6+
# without limitation the rights to use, copy, modify, merge, publish,
7+
# distribute, sublicense, and/or sell copies of the Software, and to
8+
# permit persons to whom the Software is furnished to do so, subject to
9+
# the following conditions:
10+
#
11+
# The above copyright notice and this permission notice shall be
12+
# included in all copies or substantial portions of the Software.
13+
#
14+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
module Concurrent
23+
24+
class AbstractThreadLocalVar
25+
26+
module ThreadLocalRubyStorage
27+
28+
begin
29+
require 'weakref'
30+
31+
# @!visibility private
32+
class WeakReference
33+
34+
# The object id of the object being referenced.
35+
attr_reader :referenced_object_id
36+
37+
# This implementation of a weak reference simply wraps the standard WeakRef implementation
38+
# that comes with the Ruby standard library.
39+
def initialize(obj)
40+
@referenced_object_id = obj.__id__
41+
@ref = ::WeakRef.new(obj)
42+
end
43+
44+
def object
45+
@ref.__getobj__
46+
rescue => e
47+
# Jruby implementation uses RefError while MRI uses WeakRef::RefError
48+
if (defined?(RefError) && e.is_a?(RefError)) || (defined?(::WeakRef::RefError) && e.is_a?(::WeakRef::RefError))
49+
nil
50+
else
51+
raise e
52+
end
53+
end
54+
end
55+
56+
rescue LoadError
57+
58+
require 'monitor'
59+
60+
# This is a pure ruby implementation of a weak reference. It is much more
61+
# efficient than the WeakRef implementation bundled in MRI 1.8 and 1.9
62+
# subclass Delegator which is very heavy to instantiate and utilizes a
63+
# because it does not fair amount of memory under Ruby 1.8.
64+
#
65+
# @!visibility private
66+
class WeakReference
67+
68+
# The object id of the object being referenced.
69+
attr_reader :referenced_object_id
70+
71+
# @!visibility private
72+
class ReferencePointer
73+
def initialize(object)
74+
@referenced_object_id = object.__id__
75+
add_backreference(object)
76+
end
77+
78+
def cleanup
79+
obj = ObjectSpace._id2ref(@referenced_object_id) rescue nil
80+
remove_backreference(obj) if obj
81+
end
82+
83+
def object
84+
obj = ObjectSpace._id2ref(@referenced_object_id)
85+
obj if verify_backreferences(obj)
86+
rescue RangeError
87+
nil
88+
end
89+
90+
private
91+
92+
# Verify that the object is the same one originally set for the weak reference.
93+
def verify_backreferences(obj)
94+
return nil unless supports_backreference?(obj)
95+
backreferences = obj.instance_variable_get(:@__weak_backreferences__) if obj.instance_variable_defined?(:@__weak_backreferences__)
96+
backreferences && backreferences.include?(object_id)
97+
end
98+
99+
# Add a backreference to the object.
100+
def add_backreference(obj)
101+
return unless supports_backreference?(obj)
102+
backreferences = obj.instance_variable_get(:@__weak_backreferences__) if obj.instance_variable_defined?(:@__weak_backreferences__)
103+
unless backreferences
104+
backreferences = []
105+
obj.instance_variable_set(:@__weak_backreferences__, backreferences)
106+
end
107+
backreferences << object_id
108+
end
109+
110+
# Remove backreferences from the object.
111+
def remove_backreference(obj)
112+
return unless supports_backreference?(obj)
113+
backreferences = obj.instance_variable_get(:@__weak_backreferences__) if obj.instance_variable_defined?(:@__weak_backreferences__)
114+
if backreferences
115+
backreferences.dup.delete(object_id)
116+
obj.send(:remove_instance_variable, :@__weak_backreferences__) if backreferences.empty?
117+
end
118+
end
119+
120+
def supports_backreference?(obj)
121+
obj.respond_to?(:instance_variable_get) && obj.respond_to?(:instance_variable_defined?)
122+
rescue NoMethodError
123+
false
124+
end
125+
end
126+
127+
private_constant :ReferencePointer
128+
129+
@@weak_references = {}
130+
@@lock = Monitor.new
131+
132+
# Finalizer that cleans up weak references when references are destroyed.
133+
@@reference_finalizer = lambda do |object_id|
134+
@@lock.synchronize do
135+
reference_pointer = @@weak_references.delete(object_id)
136+
reference_pointer.cleanup if reference_pointer
137+
end
138+
end
139+
140+
# Create a new weak reference to an object. The existence of the weak reference
141+
# will not prevent the garbage collector from reclaiming the referenced object.
142+
def initialize(obj)
143+
@referenced_object_id = obj.__id__
144+
@@lock.synchronize do
145+
@reference_pointer = ReferencePointer.new(obj)
146+
@@weak_references[self.object_id] = @reference_pointer
147+
end
148+
ObjectSpace.define_finalizer(self, @@reference_finalizer)
149+
end
150+
151+
# Get the reference object. If the object has already been garbage collected,
152+
# then this method will return nil.
153+
def object
154+
if @reference_pointer
155+
obj = @reference_pointer.object
156+
unless obj
157+
@@lock.synchronize do
158+
@@weak_references.delete(object_id)
159+
@reference_pointer.cleanup
160+
@reference_pointer = nil
161+
end
162+
end
163+
obj
164+
end
165+
end
166+
end
167+
end
168+
169+
private_constant :WeakReference
170+
171+
# The classes behave similar to Hashes, but the keys in the map are not strong references
172+
# and can be reclaimed by the garbage collector at any time. When a key is reclaimed, the
173+
# map entry will be removed.
174+
#
175+
# @!visibility private
176+
class WeakKeyMap
177+
178+
# Create a new map. Values added to the hash will be cleaned up by the garbage
179+
# collector if there are no other reference except in the map.
180+
def initialize
181+
@values = {}
182+
@references_to_keys_map = {}
183+
@lock = Monitor.new
184+
@reference_cleanup = lambda{|object_id| remove_reference_to(object_id)}
185+
end
186+
187+
# Get a value from the map by key. If the value has been reclaimed by the garbage
188+
# collector, this will return nil.
189+
def [](key)
190+
@lock.synchronize do
191+
rkey = ref_key(key)
192+
@values[rkey] if rkey
193+
end
194+
end
195+
196+
# Add a key/value to the map.
197+
def []=(key, value)
198+
ObjectSpace.define_finalizer(key, @reference_cleanup)
199+
@lock.synchronize do
200+
@references_to_keys_map[key.__id__] = WeakReference.new(key)
201+
@values[key.__id__] = value
202+
end
203+
end
204+
205+
# Remove the value associated with the key from the map.
206+
def delete(key)
207+
@lock.synchronize do
208+
rkey = ref_key(key)
209+
if rkey
210+
@references_to_keys_map.delete(rkey)
211+
@values.delete(rkey)
212+
else
213+
nil
214+
end
215+
end
216+
end
217+
218+
# Get an array of keys that have not yet been garbage collected.
219+
def keys
220+
@values.keys.collect{|rkey| @references_to_keys_map[rkey].object}.compact
221+
end
222+
223+
private
224+
225+
def ref_key (key)
226+
ref = @references_to_keys_map[key.__id__]
227+
if ref && ref.object
228+
ref.referenced_object_id
229+
else
230+
nil
231+
end
232+
end
233+
end
234+
235+
private_constant :WeakKeyMap
236+
end
237+
end
238+
end

lib/concurrent/utility/engine.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def ruby_engine
2121
defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
2222
end
2323

24-
def ruby_version(comparison, major, minor, patch)
24+
def ruby_version(comparison, major, minor = 0, patch = 0)
2525
result = (RUBY_VERSION.split('.').map(&:to_i) <=> [major, minor, patch])
2626
comparisons = { :== => [0],
2727
:>= => [1, 0],

0 commit comments

Comments
 (0)