|
1 |
| -`Synchronization` module provides common layer for synchronization. It provides same guaranties independent of any particular Ruby implementation. |
| 1 | +# Synchronization |
2 | 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::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. |
176 | 5 |
|
0 commit comments