Skip to content

Commit d824048

Browse files
committed
Extract safe initialization and use it for MutexAtomic{Reference,Fixnum,Boolean}
* The previous logic risked a thread seeing @__Lock__ being nil if the object was published racily.
1 parent 79a6117 commit d824048

File tree

13 files changed

+104
-133
lines changed

13 files changed

+104
-133
lines changed

ext/concurrent-ruby/com/concurrent_ruby/ext/SynchronizationLibrary.java

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,6 @@ private static boolean supportsFences() {
5555
}
5656
}
5757

58-
private static final ObjectAllocator JRUBY_OBJECT_ALLOCATOR = new ObjectAllocator() {
59-
public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
60-
return new JRubyObject(runtime, klazz);
61-
}
62-
};
63-
6458
private static final ObjectAllocator OBJECT_ALLOCATOR = new ObjectAllocator() {
6559
public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
6660
return new Object(runtime, klazz);
@@ -87,10 +81,7 @@ public void load(Ruby runtime, boolean wrap) throws IOException {
8781
RubyModule jrubyAttrVolatileModule = synchronizationModule.defineModuleUnder("JRubyAttrVolatile");
8882
jrubyAttrVolatileModule.defineAnnotatedMethods(JRubyAttrVolatile.class);
8983

90-
defineClass(runtime, synchronizationModule, "AbstractObject", "JRubyObject",
91-
JRubyObject.class, JRUBY_OBJECT_ALLOCATOR);
92-
93-
defineClass(runtime, synchronizationModule, "JRubyObject", "Object",
84+
defineClass(runtime, synchronizationModule, "AbstractObject", "Object",
9485
Object.class, OBJECT_ALLOCATOR);
9586

9687
defineClass(runtime, synchronizationModule, "Object", "AbstractLockableObject",
@@ -143,8 +134,8 @@ public static class JRubyAttrVolatile {
143134
// attempt to avoid code elimination.
144135
private static volatile int volatileField;
145136

146-
@JRubyMethod(name = "full_memory_barrier", visibility = Visibility.PUBLIC)
147-
public static IRubyObject fullMemoryBarrier(ThreadContext context, IRubyObject self) {
137+
@JRubyMethod(name = "full_memory_barrier", visibility = Visibility.PUBLIC, module = true)
138+
public static IRubyObject fullMemoryBarrier(ThreadContext context, IRubyObject module) {
148139
// Prevent reordering of ivar writes with publication of this instance
149140
if (!FULL_FENCE) {
150141
// Assuming that following volatile read and write is not eliminated it simulates fullFence.
@@ -195,16 +186,8 @@ public static IRubyObject InstanceVariableSetVolatile(
195186
}
196187
}
197188

198-
@JRubyClass(name = "JRubyObject", parent = "AbstractObject")
199-
public static class JRubyObject extends RubyObject {
200-
201-
public JRubyObject(Ruby runtime, RubyClass metaClass) {
202-
super(runtime, metaClass);
203-
}
204-
}
205-
206-
@JRubyClass(name = "Object", parent = "JRubyObject")
207-
public static class Object extends JRubyObject {
189+
@JRubyClass(name = "Object", parent = "AbstractObject")
190+
public static class Object extends RubyObject {
208191

209192
public Object(Ruby runtime, RubyClass metaClass) {
210193
super(runtime, metaClass);
@@ -220,7 +203,7 @@ public AbstractLockableObject(Ruby runtime, RubyClass metaClass) {
220203
}
221204

222205
@JRubyClass(name = "JRubyLockableObject", parent = "AbstractLockableObject")
223-
public static class JRubyLockableObject extends JRubyObject {
206+
public static class JRubyLockableObject extends AbstractLockableObject {
224207

225208
public JRubyLockableObject(Ruby runtime, RubyClass metaClass) {
226209
super(runtime, metaClass);

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
require 'concurrent/synchronization/safe_initialization'
2+
13
module Concurrent
24

35
# @!macro atomic_boolean
46
# @!visibility private
57
# @!macro internal_implementation_note
68
class MutexAtomicBoolean
9+
extend Concurrent::Synchronization::SafeInitialization
710

811
# @!macro atomic_boolean_method_initialize
912
def initialize(initial = false)
1013
super()
1114
@__Lock__ = ::Mutex.new
12-
# This synchronize should be good enough to ensure safe initialization
13-
synchronize { ns_initialize(initial) }
15+
@value = !!initial
1416
end
1517

1618
# @!macro atomic_boolean_method_value_get
@@ -54,11 +56,6 @@ def synchronize
5456
end
5557
end
5658

57-
# @!visibility private
58-
def ns_initialize(initial)
59-
@value = !!initial
60-
end
61-
6259
private
6360

6461
# @!visibility private

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require 'concurrent/synchronization/safe_initialization'
12
require 'concurrent/utility/native_integer'
23

34
module Concurrent
@@ -6,13 +7,13 @@ module Concurrent
67
# @!visibility private
78
# @!macro internal_implementation_note
89
class MutexAtomicFixnum
10+
extend Concurrent::Synchronization::SafeInitialization
911

1012
# @!macro atomic_fixnum_method_initialize
1113
def initialize(initial = 0)
1214
super()
1315
@__Lock__ = ::Mutex.new
14-
# This synchronize should be good enough to ensure safe initialization
15-
synchronize { ns_initialize(initial) }
16+
ns_set(initial)
1617
end
1718

1819
# @!macro atomic_fixnum_method_value_get
@@ -69,11 +70,6 @@ def synchronize
6970
end
7071
end
7172

72-
# @!visibility private
73-
def ns_initialize(initial)
74-
ns_set(initial)
75-
end
76-
7773
private
7874

7975
# @!visibility private

lib/concurrent-ruby/concurrent/atomic_reference/mutex_atomic.rb

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
require 'concurrent/synchronization/safe_initialization'
2+
13
module Concurrent
24

35
# @!visibility private
46
# @!macro internal_implementation_note
57
class MutexAtomicReference
8+
extend Concurrent::Synchronization::SafeInitialization
69
include AtomicDirectUpdate
710
include AtomicNumericCompareAndSetWrapper
811
alias_method :compare_and_swap, :compare_and_set
@@ -11,8 +14,7 @@ class MutexAtomicReference
1114
def initialize(value = nil)
1215
super()
1316
@__Lock__ = ::Mutex.new
14-
# This synchronize should be good enough to ensure safe initialization
15-
synchronize { ns_initialize(value) }
17+
@value = value
1618
end
1719

1820
# @!macro atomic_reference_method_get
@@ -59,10 +61,5 @@ def synchronize
5961
@__Lock__.synchronize { yield }
6062
end
6163
end
62-
63-
# @!visibility private
64-
def ns_initialize(value)
65-
@value = value
66-
end
6764
end
6865
end

lib/concurrent-ruby/concurrent/synchronization/abstract_object.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ module Synchronization
44
# @!visibility private
55
# @!macro internal_implementation_note
66
class AbstractObject
7-
8-
# @abstract has to be implemented based on Ruby runtime
97
def initialize
10-
raise NotImplementedError
8+
# nothing to do
119
end
1210

1311
# @!visibility private
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module Concurrent
2+
module Synchronization
3+
case
4+
when Concurrent.on_cruby?
5+
def self.full_memory_barrier
6+
# relying on undocumented behavior of CRuby, GVL acquire has lock which ensures visibility of ivars
7+
# https://github.com/ruby/ruby/blob/ruby_2_2/thread_pthread.c#L204-L211
8+
end
9+
10+
when Concurrent.on_jruby?
11+
require 'concurrent/utility/native_extension_loader'
12+
def self.full_memory_barrier
13+
JRubyAttrVolatile.full_memory_barrier
14+
end
15+
16+
when Concurrent.on_truffleruby?
17+
def self.full_memory_barrier
18+
TruffleRuby.full_memory_barrier
19+
end
20+
21+
else
22+
warn 'Possibly unsupported Ruby implementation'
23+
def self.full_memory_barrier
24+
end
25+
end
26+
end
27+
end

lib/concurrent-ruby/concurrent/synchronization/jruby_object.rb

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ module Concurrent
22
module Synchronization
33

44
if Concurrent.on_jruby? && Concurrent.java_extensions_loaded?
5-
65
# @!visibility private
76
module JRubyAttrVolatile
87
def self.included(base)
@@ -30,16 +29,7 @@ def #{name}=(value)
3029
end
3130
end
3231
end
33-
34-
# @!visibility private
35-
# @!macro internal_implementation_note
36-
class JRubyObject < AbstractObject
37-
include JRubyAttrVolatile
38-
39-
def initialize
40-
# nothing to do
41-
end
42-
end
4332
end
33+
4434
end
4535
end

lib/concurrent-ruby/concurrent/synchronization/mri_object.rb

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,7 @@ def #{name}=(value)
2424
names.map { |n| [n, :"#{n}="] }.flatten
2525
end
2626
end
27-
28-
def full_memory_barrier
29-
# relying on undocumented behavior of CRuby, GVL acquire has lock which ensures visibility of ivars
30-
# https://github.com/ruby/ruby/blob/ruby_2_2/thread_pthread.c#L204-L211
31-
end
3227
end
3328

34-
# @!visibility private
35-
# @!macro internal_implementation_note
36-
class MriObject < AbstractObject
37-
include MriAttrVolatile
38-
39-
def initialize
40-
# nothing to do
41-
end
42-
end
4329
end
4430
end

lib/concurrent-ruby/concurrent/synchronization/object.rb

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
1+
require 'concurrent/synchronization/safe_initialization'
2+
require 'concurrent/synchronization/volatile'
13
require 'concurrent/atomic/atomic_reference'
24

35
module Concurrent
46
module Synchronization
57

6-
# @!visibility private
7-
# @!macro internal_implementation_note
8-
ObjectImplementation = case
9-
when Concurrent.on_cruby?
10-
MriObject
11-
when Concurrent.on_jruby?
12-
JRubyObject
13-
when Concurrent.on_truffleruby?
14-
TruffleRubyObject
15-
else
16-
warn 'Possibly unsupported Ruby implementation'
17-
MriObject
18-
end
19-
private_constant :ObjectImplementation
20-
218
# Abstract object providing final, volatile, ans CAS extensions to build other concurrent abstractions.
229
# - final instance variables see {Object.safe_initialization!}
2310
# - volatile instance variables see {Object.attr_volatile}
2411
# - volatile instance variables see {Object.attr_atomic}
25-
class Object < ObjectImplementation
12+
class Object < AbstractObject
13+
include Volatile
14+
2615
# TODO make it a module if possible
2716

2817
# @!method self.attr_volatile(*names)
@@ -38,36 +27,12 @@ def initialize
3827
__initialize_atomic_fields__
3928
end
4029

41-
# By calling this method on a class, it and all its children are marked to be constructed safely. Meaning that
42-
# all writes (ivar initializations) are made visible to all readers of newly constructed object. It ensures
43-
# same behaviour as Java's final fields.
44-
# @example
45-
# class AClass < Concurrent::Synchronization::Object
46-
# safe_initialization!
47-
#
48-
# def initialize
49-
# @AFinalValue = 'value' # published safely, does not have to be synchronized
50-
# end
51-
# end
52-
# @return [true]
5330
def self.safe_initialization!
54-
# define only once, and not again in children
55-
return if safe_initialization?
56-
57-
# @!visibility private
58-
def self.new(*args, &block)
59-
object = super(*args, &block)
60-
ensure
61-
object.full_memory_barrier if object
62-
end
63-
64-
@safe_initialization = true
31+
extend SafeInitialization
6532
end
6633

67-
# @return [true, false] if this class is safely initialized.
6834
def self.safe_initialization?
69-
@safe_initialization = false unless defined? @safe_initialization
70-
@safe_initialization || (superclass.respond_to?(:safe_initialization?) && superclass.safe_initialization?)
35+
self.singleton_class < SafeInitialization
7136
end
7237

7338
# For testing purposes, quite slow. Injects assert code to new method which will raise if class instance contains
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require 'concurrent/synchronization/full_memory_barrier'
2+
3+
module Concurrent
4+
module Synchronization
5+
6+
# @!visibility private
7+
# @!macro internal_implementation_note
8+
#
9+
# By extending this module, a class and all its children are marked to be constructed safely. Meaning that
10+
# all writes (ivar initializations) are made visible to all readers of newly constructed object. It ensures
11+
# same behaviour as Java's final fields.
12+
#
13+
# Due to using Kernel#extend, the module is not included again if already present in the ancestors,
14+
# which avoids extra overhead.
15+
#
16+
# @example
17+
# class AClass < Concurrent::Synchronization::Object
18+
# extend Concurrent::Synchronization::SafeInitialization
19+
#
20+
# def initialize
21+
# @AFinalValue = 'value' # published safely, #foo will never return nil
22+
# end
23+
#
24+
# def foo
25+
# @AFinalValue
26+
# end
27+
# end
28+
module SafeInitialization
29+
def new(*args, &block)
30+
super(*args, &block)
31+
ensure
32+
Concurrent::Synchronization.full_memory_barrier
33+
end
34+
end
35+
end
36+
end

0 commit comments

Comments
 (0)