Skip to content

Commit db5fcfb

Browse files
author
Petr Chalupa
committed
Merge pull request #421 from ruby-concurrency/synchronization
Documentation and few updates and class conversions
2 parents 668243e + eb72049 commit db5fcfb

32 files changed

+456
-432
lines changed

doc/synchronization-notes.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Concurrent Ruby Notes
2+
3+
## Locks
4+
5+
Concurrent Ruby also has an internal extension of `Object` called
6+
`LockableObject`, which provides same synchronization primitives as Java's
7+
Object: `synchronize(&block)`, `wait(timeout = nil)`,
8+
`wait_until(timeout = nil, &condition)`, `signal`, `broadcast`. This class is
9+
intended for internal use in `concurrent-ruby` only and it does not support
10+
subclassing (since it cannot protect its lock from its children, for more
11+
details see [this article](http://wiki.apidesign.org/wiki/Java_Monitor)). It has
12+
minimal interface to be able to use directly locking available on given
13+
platforms.
14+
15+
For non-internal use there is `Lock` and `Condition` implementation in
16+
`Synchronization` namespace, a condition can be obtained with `new_condition`
17+
method on `Lock`. So far their implementation is naive and requires more work.
18+
API is not expected to change.
19+
20+
## Method names conventions
21+
22+
Methods starting with `ns_` are marking methods that are not using
23+
synchronization by themselves, they have to be used inside synchronize block.
24+
They are usually used in pairs to separate the synchronization from behavior and
25+
to allow to call methods in the same object without double locking.
26+
27+
``` ruby
28+
class Node
29+
# ...
30+
def left
31+
synchronize { ns_left }
32+
end
33+
34+
def right
35+
synchronize { ns_right }
36+
end
37+
38+
def to_a
39+
# avoids double locking
40+
synchronize { [ns_left, ns_right] }
41+
end
42+
43+
private
44+
45+
def ns_left
46+
@left
47+
end
48+
49+
def ns_right
50+
@right
51+
end
52+
# ...
53+
end
54+
```
55+
## Piggybacking
56+
57+
Any write executed before volatile write based on program-order is visible to
58+
the volatile read as well, which allows
59+
[piggybacking](http://stackoverflow.com/questions/8769570/volatile-piggyback-is-this-enough-for-visiblity).
60+
Because it creates synchronizes-with (JMM term) order between volatile write
61+
and read, which participates in creating happens-before order.
62+
63+
This trick is used in some of the abstractions, to avoid unnecessary
64+
synchronization or volatile declarations.

doc/synchronization.md

Lines changed: 3 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,5 @@
1-
`Synchronization` module provides common layer for synchronization. It provides same guaranties independent of any particular Ruby implementation.
1+
# Synchronization
22

3-
*This is a new module, it is expected to fully stabilize for 1.0 release.*
4-
5-
## Synchronization::Object
6-
7-
Provides common parent for all objects which need to be synchronized or be using other synchronization tools. It provides:
8-
9-
- Synchronized block
10-
- Methods for waiting and signaling
11-
- Volatile fields
12-
- Ensure visibility of final fields
13-
- Fields with CAS operations
14-
15-
## Synchronized block
16-
17-
`Synchronization::Object` provides private method `#synchronize(&block)`. For a given object only one Thread can enter one of the blocks synchronized against this object. Object is locked when a thread enters one of the synchronized blocks.
18-
19-
Example of a simple counter which can be used by multiple threads:
20-
21-
```ruby
22-
class SafeCounter < Concurrent::Synchronization::Object
23-
def initialize
24-
super
25-
synchronize { @count = 0 }
26-
end
27-
28-
def increment
29-
synchronize { @count += 1 }
30-
end
31-
32-
def count
33-
synchronize { @count }
34-
end
35-
end
36-
```
37-
38-
### Naming conventions
39-
40-
Methods starting with `ns_` are marking methods that are not using synchronization by themselves, they have to be used inside synchronize block. They are usually used in pairs to separate the synchronization from behavior:
41-
42-
```ruby
43-
def compute
44-
service.report synchronize { ns_compute }
45-
end
46-
47-
private
48-
49-
def ns_compute
50-
ns_compute_reduce ns_compute_map
51-
end
52-
```
53-
where `compute` defines how is it synchronized and `ns_compute` handles the behavior (in this case the computation). `ns_` methods should only call other `ns_` methods or `pr_` methods. They can call normal methods on other objects, but that should be done with care (better to avoid) because the thread escapes this object while the lock is still held, which can lead to deadlock. That's why the `report` method is called in `compute` and not in `ns_compute`.
54-
55-
`pr_` methods are pure functions they can be used in and outside of synchronized blocks.
56-
57-
## Methods for waiting and signaling
58-
59-
Sometimes while already inside the synchronized block some condition is not met. Then the thread needs to wait (releasing the lock) until the condition is met. The waiting thread is then signaled that it can continue.
60-
61-
To fulfill these needs there are private methods:
62-
63-
- `ns_wait` {include:Concurrent::Synchronization::AbstractLockableObject#ns_wait}
64-
- `ns_wait_until` {include:Concurrent::Synchronization::AbstractLockableObject#ns_wait_until}
65-
- `ns_signal` {include:Concurrent::Synchronization::AbstractLockableObject#ns_signal}
66-
- `ns_broadcast` {include:Concurrent::Synchronization::AbstractLockableObject#ns_broadcast}
67-
68-
All methods have to be called inside synchronized block.
69-
70-
## Volatile fields
71-
72-
`Synchronization::Object` can have volatile fields (Java semantic). They are defined by `attr_volatile :field_name`. `attr_volatile` defines reader and writer with the `field_name`. Any write is always immediately visible for any subsequent reads of the same field.
73-
74-
## Ensure visibility of final fields
75-
76-
Instance variables assigned only once in `initialize` method are not guaranteed to be visible to all threads. For that user can call `ensure_ivar_visibility!` method, like in following example taken from `Edge::AbstractPromise` implementation:
77-
78-
```ruby
79-
class AbstractPromise < Synchronization::Object
80-
def initialize(future, *args, &block)
81-
super(*args, &block)
82-
@Future = future
83-
ensure_ivar_visibility!
84-
end
85-
# ...
86-
end
87-
```
88-
89-
### Naming conventions
90-
91-
Instance variables with camel case names are final and never reassigned, e.g. `@FinalVariable`.
92-
93-
## Fields with CAS operations
94-
95-
They are not supported directly, but AtomicReference can be stored in final field and then CAS operations can be done on it, like in following example taken from `Edge::Event` implementation:
96-
97-
```ruby
98-
class Event < Synchronization::Object
99-
extend FutureShortcuts
100-
101-
def initialize(promise, default_executor = :io)
102-
@Promise = promise
103-
@DefaultExecutor = default_executor
104-
@Touched = AtomicBoolean.new(false)
105-
super()
106-
ensure_ivar_visibility!
107-
end
108-
# ...
109-
def touch
110-
# distribute touch to promise only once
111-
@Promise.touch if @Touched.make_true
112-
self
113-
end
114-
# ...
115-
end
116-
```
117-
118-
Operations on `@Touched` field have volatile semantic.
119-
120-
## Memory model
121-
122-
*Intended for further revision, and extension.*
123-
124-
When writing libraries in `concurrent-ruby` we are reasoning based on following memory model which is further extended by features provided in `Synchronization::Object` (described above).
125-
126-
The memory model is constructed based on our best effort and knowledge of the 3 main Ruby implementations (CRuby, JRuby, Rubinius). When considering certain aspect we always choose the weakest guarantee (e.g. local variable updates are always visible in CRuby but not in JRuby, so in this case JRuby behavior is picked). If some Ruby behavior is omitted here it is considered unsafe for use in parallel environment (Reasons may be lack of information, or difficulty of verification).
127-
128-
This takes in account following implementations:
129-
130-
- CRuby 1.9 - 2.2 (no differences found)
131-
- JRuby 1.7
132-
- JRuby 9 *not examined yet, same behavior as in 1.7 assumed*
133-
- Rubinius 2.5
134-
135-
We are interested in following behaviors:
136-
137-
- **volatility** - in Java's sense. Any written value is immediately visible to any subsequent reads including all writes leading to this value.
138-
- **atomicity** - operation is either done or not as a whole.
139-
140-
### Variables
141-
142-
- **Local variables** - atomic assignment (only Integer and Object), non-volatile.
143-
- Consequence: a lambda defined on `thread1` executing on `thread2` may not see updated values in local variables captured in its closure.
144-
- Reason: local variables are non-volatile on Jruby and Rubinius.
145-
- **Instance variables** - atomic assignment (only Integer and Object), non-volatile.
146-
- Consequence: Different thread may see old values; different thread may see not fully-initialized object.
147-
- Reason: local variables are non-volatile on Jruby and Rubinius.
148-
- **Constants** - atomic assignment, volatile.
149-
- **Assignments of Float** may not be atomic (some implementations (e.g. Truffle) may use native double).
150-
151-
Other:
152-
153-
- **Global variables** - we don't use them, omitted (atomic and volatile on JRuby and CRuby, Rubinius unknown)
154-
- **Class variables** - we don't use them, omitted (atomic and volatile on JRuby and CRuby, Rubinius unknown)
155-
156-
### Assumptions
157-
158-
Following operations are **assumed** thread-safe, volatile and atomic on all implementations:
159-
160-
- Class definition
161-
- Method definition
162-
- Library requirement
163-
164-
It's best practice though to eager load before going into parallel part of an application.
165-
166-
### Issues to be aware of
167-
168-
- **Initialization** - Since instance variables are not volatile and a particular implementation may preinitialize values with nils, based on shapes it already saw, a second thread obtaining reference to newly constructed may still see old preinitialized values instead of values set in `initialize` method. To fix this `ensure_ivar_visibility!` can be used or the object can be safely published in a volatile field.
169-
- **`||=`, `+=` and similar** - are not atomic.
170-
171-
### Notes/Sources on implementations
172-
173-
- [JRuby wiki page on concurrency](https://github.com/jruby/jruby/wiki/Concurrency-in-jruby)
174-
- [Rubinius page on concurrency](http://rubini.us/doc/en/systems/concurrency/)
175-
- CRuby has GVL. Any GVL release and acquire uses lock which means that all writes done by a releasing thread will be visible to the second acquiring thread. See: <https://github.com/ruby/ruby/blob/ruby_2_2/thread_pthread.c#L101-L107>
3+
[This document](https://docs.google.com/document/d/1pVzU8w_QF44YzUCCab990Q_WZOdhpKolCIHaiXG-sPw/edit?usp=sharing)
4+
is moved to Google documents. It will be moved here once final and stabilized.
1765

ext/com/concurrent_ruby/ext/SynchronizationLibrary.java

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.jruby.RubyClass;
77
import org.jruby.RubyModule;
88
import org.jruby.RubyObject;
9+
import org.jruby.RubyBasicObject;
910
import org.jruby.anno.JRubyClass;
1011
import org.jruby.anno.JRubyMethod;
1112
import org.jruby.runtime.ObjectAllocator;
@@ -47,6 +48,9 @@ public void load(Ruby runtime, boolean wrap) throws IOException {
4748
defineModule("Concurrent").
4849
defineModuleUnder("Synchronization");
4950

51+
RubyModule jrubyAttrVolatileModule = synchronizationModule.defineModuleUnder("JRubyAttrVolatile");
52+
jrubyAttrVolatileModule.defineAnnotatedMethods(JRubyAttrVolatile.class);
53+
5054
defineClass(runtime, synchronizationModule, "AbstractObject", "JRubyObject",
5155
JRubyObject.class, JRUBY_OBJECT_ALLOCATOR);
5256

@@ -81,21 +85,16 @@ private RubyClass defineClass(Ruby runtime, RubyModule namespace, String parentN
8185
// SynchronizedVariableAccessor wraps with synchronized block, StampedVariableAccessor uses fullFence or
8286
// volatilePut
8387

84-
@JRubyClass(name = "JRubyObject", parent = "AbstractObject")
85-
public static class JRubyObject extends RubyObject {
86-
private static volatile ThreadContext threadContext = null;
87-
88-
public JRubyObject(Ruby runtime, RubyClass metaClass) {
89-
super(runtime, metaClass);
90-
}
88+
// module JRubyAttrVolatile
89+
public static class JRubyAttrVolatile {
9190

92-
@JRubyMethod
93-
public IRubyObject initialize(ThreadContext context) {
94-
return this;
95-
}
91+
// volatile threadContext is used as a memory barrier per the JVM memory model happens-before semantic
92+
// on volatile fields. any volatile field could have been used but using the thread context is an
93+
// attempt to avoid code elimination.
94+
private static volatile ThreadContext threadContext = null;
9695

9796
@JRubyMethod(name = "full_memory_barrier", visibility = Visibility.PRIVATE)
98-
public IRubyObject fullMemoryBarrier(ThreadContext context) {
97+
public static IRubyObject fullMemoryBarrier(ThreadContext context, IRubyObject self) {
9998
// Prevent reordering of ivar writes with publication of this instance
10099
if (UnsafeHolder.U == null || !UnsafeHolder.SUPPORTS_FENCES) {
101100
// Assuming that following volatile read and write is not eliminated it simulates fullFence.
@@ -109,35 +108,43 @@ public IRubyObject fullMemoryBarrier(ThreadContext context) {
109108
}
110109

111110
@JRubyMethod(name = "instance_variable_get_volatile", visibility = Visibility.PROTECTED)
112-
public IRubyObject instanceVariableGetVolatile(ThreadContext context, IRubyObject name) {
111+
public static IRubyObject instanceVariableGetVolatile(ThreadContext context, IRubyObject self, IRubyObject name) {
113112
// Ensure we ses latest value with loadFence
114113
if (UnsafeHolder.U == null || !UnsafeHolder.SUPPORTS_FENCES) {
115114
// piggybacking on volatile read, simulating loadFence
116115
final ThreadContext oldContext = threadContext;
117-
return instance_variable_get(context, name);
116+
return ((RubyBasicObject)self).instance_variable_get(context, name);
118117
} else {
119118
UnsafeHolder.loadFence();
120-
return instance_variable_get(context, name);
119+
return ((RubyBasicObject)self).instance_variable_get(context, name);
121120
}
122121
}
123122

124123
@JRubyMethod(name = "instance_variable_set_volatile", visibility = Visibility.PROTECTED)
125-
public IRubyObject InstanceVariableSetVolatile(ThreadContext context, IRubyObject name, IRubyObject value) {
124+
public static IRubyObject InstanceVariableSetVolatile(ThreadContext context, IRubyObject self, IRubyObject name, IRubyObject value) {
126125
// Ensure we make last update visible
127126
if (UnsafeHolder.U == null || !UnsafeHolder.SUPPORTS_FENCES) {
128127
// piggybacking on volatile write, simulating storeFence
129-
final IRubyObject result = instance_variable_set(name, value);
128+
final IRubyObject result = ((RubyBasicObject)self).instance_variable_set(name, value);
130129
threadContext = context;
131130
return result;
132131
} else {
133132
// JRuby uses StampedVariableAccessor which calls fullFence
134133
// so no additional steps needed.
135134
// See https://github.com/jruby/jruby/blob/master/core/src/main/java/org/jruby/runtime/ivars/StampedVariableAccessor.java#L151-L159
136-
return instance_variable_set(name, value);
135+
return ((RubyBasicObject)self).instance_variable_set(name, value);
137136
}
138137
}
139138
}
140139

140+
@JRubyClass(name = "JRubyObject", parent = "AbstractObject")
141+
public static class JRubyObject extends RubyObject {
142+
143+
public JRubyObject(Ruby runtime, RubyClass metaClass) {
144+
super(runtime, metaClass);
145+
}
146+
}
147+
141148
@JRubyClass(name = "Object", parent = "JRubyObject")
142149
public static class Object extends JRubyObject {
143150

lib/concurrent/async.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ def new(*args, &block)
299299
#
300300
# @!visibility private
301301
class AsyncDelegator < Synchronization::LockableObject
302+
safe_initialization!
302303

303304
# Create a new delegator object wrapping the given delegate.
304305
#
@@ -308,7 +309,6 @@ def initialize(delegate)
308309
@delegate = delegate
309310
@queue = []
310311
@executor = Concurrent.global_io_executor
311-
ensure_ivar_visibility!
312312
end
313313

314314
# Delegates method calls to the wrapped object.

lib/concurrent/atom.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,10 @@ class Atom < Synchronization::Object
111111
#
112112
# @raise [ArgumentError] if the validator is not a `Proc` (when given)
113113
def initialize(value, opts = {})
114+
super()
114115
@Validator = opts.fetch(:validator, -> v { true })
115116
self.observers = Collection::CopyOnNotifyObserverSet.new
116-
super(value)
117+
self.value = value
117118
end
118119

119120
# @!method value

lib/concurrent/atomic/atomic_boolean.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
require 'concurrent/atomic/mutex_atomic_boolean'
2-
require 'concurrent/utility/native_extension_loader'
2+
require 'concurrent/synchronization'
33

44
module Concurrent
55

lib/concurrent/atomic/atomic_fixnum.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
require 'concurrent/atomic/mutex_atomic_fixnum'
2-
require 'concurrent/utility/native_extension_loader'
2+
require 'concurrent/synchronization'
33

44
module Concurrent
55

lib/concurrent/atomic/atomic_reference.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
require 'concurrent/utility/native_extension_loader'
1+
require 'concurrent/synchronization'
22
require 'concurrent/utility/engine'
33
require 'concurrent/atomic_reference/concurrent_update_error'
44
require 'concurrent/atomic_reference/mutex_atomic'

lib/concurrent/atomic/semaphore.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
require 'concurrent/atomic/mutex_semaphore'
2-
require 'concurrent/utility/native_extension_loader'
2+
require 'concurrent/synchronization'
33

44
module Concurrent
55

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require 'concurrent/atomic_reference/rbx'

0 commit comments

Comments
 (0)