Skip to content

Commit 310b7df

Browse files
committed
Synchronization module documentation
1 parent 47cde7b commit 310b7df

File tree

6 files changed

+131
-11
lines changed

6 files changed

+131
-11
lines changed

doc/synchronization.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
`Synchronization` module provides common layer for synchronization. It provides same guaranties independent of any particular Ruby implementation.
2+
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::AbstractObject#ns_wait}
64+
- `ns_wait_until` {include:Concurrent::Synchronization::AbstractObject#ns_wait_until}
65+
- `ns_signal` {include:Concurrent::Synchronization::AbstractObject#ns_signal}
66+
- `ns_broadcast` {include:Concurrent::Synchronization::AbstractObject#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.
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+
## Memory model (sort of)
119+
120+
// TODO
121+

lib/concurrent/configuration.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def self.global_timer_set
106106
end
107107

108108
# General access point to global executors.
109-
# @param [Symbol, Executor] maps symbols:
109+
# @param [Symbol, Executor] executor_identifier symbols:
110110
# - :fast - {Concurrent.global_fast_executor}
111111
# - :io - {Concurrent.global_io_executor}
112112
# - :immediate - {Concurrent.global_immediate_executor}

lib/concurrent/edge/future.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ def wait(timeout = nil)
113113
end
114114

115115
def touch
116-
if (promise = promise_to_touch)
117-
promise.touch
118-
end
116+
# distribute touch to promise only once
117+
@Promise.touch if @Touched.make_true
118+
self
119119
end
120120

121121
def state
@@ -232,10 +232,6 @@ def ns_completed?
232232
ns_state == :completed
233233
end
234234

235-
def promise_to_touch
236-
@Promise if @Touched.make_true
237-
end
238-
239235
def pr_chain(default_executor, executor = nil, &callback)
240236
ChainPromise.new(self, default_executor, executor || default_executor, &callback).future
241237
end

lib/concurrent/synchronization.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'concurrent/synchronization/rbx_object'
88

99
module Concurrent
10+
# {include:file:doc/synchronization.md}
1011
module Synchronization
1112
Implementation = case
1213
when Concurrent.on_jruby?

lib/concurrent/synchronization/abstract_object.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def ns_wait(timeout = nil)
9191
raise NotImplementedError
9292
end
9393

94-
# Signal one waiting thread
94+
# Signal one waiting thread.
9595
# @return [self]
9696
# @note only to be used inside synchronized block
9797
# @note to provide direct access to this method in a descendant add method
@@ -104,7 +104,7 @@ def ns_signal
104104
raise NotImplementedError
105105
end
106106

107-
# Broadcast to all waiting threads
107+
# Broadcast to all waiting threads.
108108
# @return [self]
109109
# @note only to be used inside synchronized block
110110
# @note to provide direct access to this method in a descendant add method

lib/concurrent/synchronization/java_object.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ module Synchronization
55
require 'jruby'
66

77
class JavaObject < AbstractObject
8-
def ensure_ivar_visibility!
8+
private
9+
10+
def ensure_ivar_visibility! # TODO move to java version
911
# relying on undocumented behavior of JRuby, ivar access is volatile
1012
end
1113
end

0 commit comments

Comments
 (0)