Skip to content

Commit df217b2

Browse files
committed
Merge branch 'master' into synchronization
* master: (5 commits) Deprecate timeout ...
2 parents 3dc0e87 + 8266271 commit df217b2

File tree

8 files changed

+205
-114
lines changed

8 files changed

+205
-114
lines changed

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,22 +199,22 @@ any platform. *Documentation is forthcoming...*
199199

200200
```
201201
*MRI only*
202-
rake build:native # Build concurrent-ruby-ext-<version>-<platform>.gem into the pkg directory
203-
rake compile:extension # Compile extension
202+
bundle exec rake build:native # Build concurrent-ruby-ext-<version>-<platform>.gem into the pkg dir
203+
bundle exec rake compile:extension # Compile extension
204204
205205
*JRuby only*
206-
rake build # Build JRuby-specific core gem (alias for `build:core`)
207-
rake build:core # Build concurrent-ruby-<version>-java.gem into the pkg directory
206+
bundle exec rake build # Build JRuby-specific core gem (alias for `build:core`)
207+
bundle exec rake build:core # Build concurrent-ruby-<version>-java.gem into the pkg directory
208208
209209
*All except JRuby*
210-
rake build # Build core and extension gems
211-
rake build:core # Build concurrent-ruby-<version>.gem into the pkg directory
212-
rake build:ext # Build concurrent-ruby-ext-<version>.gem into the pkg directory
210+
bundle exec rake build # Build core and extension gems
211+
bundle exec rake build:core # Build concurrent-ruby-<version>.gem into the pkg directory
212+
bundle exec rake build:ext # Build concurrent-ruby-ext-<version>.gem into the pkg directory
213213
214214
*All*
215-
rake clean # Remove any temporary products
216-
rake clobber # Remove any generated file
217-
rake compile # Compile all the extensions
215+
bundle exec rake clean # Remove any temporary products
216+
bundle exec rake clobber # Remove any generated file
217+
bundle exec rake compile # Compile all the extensions
218218
```
219219

220220
## Maintainers

doc/tvar.md

Lines changed: 78 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
`TVar` and `atomically` implement a software transactional memory. A `TVar` is a single
2-
item container that always contains exactly one value. The `atomically` method
3-
allows you to modify a set of `TVar` objects with the guarantee that all of the
4-
updates are collectively atomic - they either all happen or none of them do -
5-
consistent - a `TVar` will never enter an illegal state - and isolated - atomic
6-
blocks never interfere with each other when they are running. You may recognise
7-
these properties from database transactions.
1+
`TVar` and `atomically` implement a software transactional memory. A `TVar` is a
2+
single item container that always contains exactly one value. The `atomically`
3+
method allows you to modify a set of `TVar` objects with the guarantee that all
4+
of the updates are collectively atomic - they either all happen or none of them
5+
do - consistent - a `TVar` will never enter an illegal state - and isolated -
6+
atomic blocks never interfere with each other when they are running. You may
7+
recognise these properties from database transactions.
88

99
There are some very important and unusual semantics that you must be aware of:
1010

@@ -24,7 +24,10 @@ We implement nested transactions by flattening.
2424
We only support strong isolation if you use the API correctly. In order words,
2525
we do not support strong isolation.
2626

27-
Our implementation uses a very simple two-phased locking with versioned locks algorithm, as per [1]. In the future we will look at more advanced algorithms, contention management and using existing Java implementations when in JRuby.
27+
Our implementation uses a very simple two-phased locking with versioned locks
28+
algorithm and lazy writes, as per [1]. In the future we will look at more
29+
advanced algorithms, contention management and using existing Java
30+
implementations when in JRuby.
2831

2932
See:
3033

@@ -150,46 +153,94 @@ repeated execution.
150153

151154
## Evaluation
152155

153-
We evaluated the performance of our `TVar` implementation using a bank account simulation with a range of synchronisation implementations. The simulation maintains a set of bank account totals, and runs transactions that either get a summary statement of multiple accounts (a read-only operation) or transfers a sum from one account to another (a read-write operation).
154-
155-
We implemented a bank that does not use any synchronisation (and so creates inconsistent totals in accounts), one that uses a single global (or 'coarse') lock (and so won't scale at all), one that uses one lock per account (and so has a complicated system for locking in the correct order) and one using our `TVar` and `atomically`.
156-
157-
We ran 1 million transactions divided equally between a varying number of threads on a system that has at least that many physical cores. The transactions are made up of a varying mixture of read-only and read-write transactions. We ran each set of transactions thirty times, discarding the first ten and then taking an algebraic mean. These graphs show only the simple mean. Our `tvars-experiments` branch includes the benchmark used, full details of the test system, and all the raw data.
158-
159-
Using JRuby using 75% read-write transactions, we can compare how the different implementations of bank accounts scales to more cores. That is, how much faster it runs if you use more cores.
156+
We evaluated the performance of our `TVar` implementation using a bank account
157+
simulation with a range of synchronisation implementations. The simulation
158+
maintains a set of bank account totals, and runs transactions that either get a
159+
summary statement of multiple accounts (a read-only operation) or transfers a
160+
sum from one account to another (a read-write operation).
161+
162+
We implemented a bank that does not use any synchronisation (and so creates
163+
inconsistent totals in accounts), one that uses a single global (or 'coarse')
164+
lock (and so won't scale at all), one that uses one lock per account (and so has
165+
a complicated system for locking in the correct order) and one using our `TVar`
166+
and `atomically`.
167+
168+
We ran 1 million transactions divided equally between a varying number of
169+
threads on a system that has at least that many physical cores. The transactions
170+
are made up of a varying mixture of read-only and read-write transactions. We
171+
ran each set of transactions thirty times, discarding the first ten and then
172+
taking an algebraic mean. These graphs show only the simple mean. Our `tvars-
173+
experiments` branch includes the benchmark used, full details of the test
174+
system, and all the raw data.
175+
176+
Using JRuby using 75% read-write transactions, we can compare how the different
177+
implementations of bank accounts scales to more cores. That is, how much faster
178+
it runs if you use more cores.
160179

161180
![](https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/implementation-scalability.png)
162181

163-
We see that the coarse lock implementation does not scale at all, and in fact with more cores only wastes more time in contention for the single global lock. We see that the unsynchronised implementation doesn't seem to scale well - which is strange as there should be no overhead, but we'll explain that in a second. We see that the fine lock implementation seems to scale better, and that the `TVar` implementation scales the best.
182+
We see that the coarse lock implementation does not scale at all, and in fact
183+
with more cores only wastes more time in contention for the single global lock.
184+
We see that the unsynchronised implementation doesn't seem to scale well - which
185+
is strange as there should be no overhead, but we'll explain that in a second.
186+
We see that the fine lock implementation seems to scale better, and that the
187+
`TVar` implementation scales the best.
164188

165189
So the `TVar` implementation *scales* very well, but how absolutely fast is it?
166190

167191
![](https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/implementation-absolute.png)
168192

169-
Well, that's the downside. The unsynchronised implementation doesn't scale well because it's so fast in the first place, and probably because we're bound on access to the memory - the threads don't have much work to do, so no matter how many threads we have the system is almost always reaching out to the L3 cache or main memory. However remember that the unsynchronised implementation isn't correct - the totals are wrong at the end. The coarse lock implementation has an overhead of locking and unlocking. The fine lock implementation has a greater overhead as as the locking scheme is complicated to avoid deadlock. It scales better, however, actually allowing transactions to be processed in parallel. The `TVar` implementation has a greater overhead still - and it's pretty huge. That overhead is the cost for the simple programming model of an atomic block.
170-
171-
So that's what `TVar` gives you at the moment - great scalability, but it has a high overhead. That's pretty much the state of software transactional memory in general. Perhaps hardware transactional memory will help us, or perhaps we're happy anyway with the simpler and safer programming model that the `TVar` gives us.
172-
173-
We can also use this experiment to compare different implementations of Ruby. We looked at just the `TVar` implementation and compared MRI 2.1.1, Rubinius 2.2.6, and JRuby 1.7.11, again at 75% write transactions.
193+
Well, that's the downside. The unsynchronised implementation doesn't scale well
194+
because it's so fast in the first place, and probably because we're bound on
195+
access to the memory - the threads don't have much work to do, so no matter how
196+
many threads we have the system is almost always reaching out to the L3 cache or
197+
main memory. However remember that the unsynchronised implementation isn't
198+
correct - the totals are wrong at the end. The coarse lock implementation has an
199+
overhead of locking and unlocking. The fine lock implementation has a greater
200+
overhead as as the locking scheme is complicated to avoid deadlock. It scales
201+
better, however, actually allowing transactions to be processed in parallel. The
202+
`TVar` implementation has a greater overhead still - and it's pretty huge. That
203+
overhead is the cost for the simple programming model of an atomic block.
204+
205+
So that's what `TVar` gives you at the moment - great scalability, but it has a
206+
high overhead. That's pretty much the state of software transactional memory in
207+
general. Perhaps hardware transactional memory will help us, or perhaps we're
208+
happy anyway with the simpler and safer programming model that the `TVar` gives
209+
us.
210+
211+
We can also use this experiment to compare different implementations of Ruby. We
212+
looked at just the `TVar` implementation and compared MRI 2.1.1, Rubinius 2.2.6,
213+
and JRuby 1.7.11, again at 75% write transactions.
174214

175215
![](https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/ruby-scalability.png)
176216

177-
We see that MRI provides no scalability, due to the global interpreter lock (GIL). JRuby seems to scale better than Rubinius for this workload (there are of course other workloads).
217+
We see that MRI provides no scalability, due to the global interpreter lock
218+
(GIL). JRuby seems to scale better than Rubinius for this workload (there are of
219+
course other workloads).
178220

179-
As before we should also look at the absolute performance, not just the scalability.
221+
As before we should also look at the absolute performance, not just the
222+
scalability.
180223

181224
![](https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/ruby-absolute.png)
182225

183-
Again, JRuby seems to be faster than Rubinius for this experiment. Interestingly, Rubinius looks slower than MRI for 1 core, but we can get around that by using more cores.
226+
Again, JRuby seems to be faster than Rubinius for this experiment.
227+
Interestingly, Rubinius looks slower than MRI for 1 core, but we can get around
228+
that by using more cores.
184229

185-
We've used 75% read-write transactions throughout. We'll just take a quick look at how the scalability varies for different workloads, for scaling between 1 and 2 threads. We'll admit that we used 75% read-write just because it emphasised the differences.
230+
We've used 75% read-write transactions throughout. We'll just take a quick look
231+
at how the scalability varies for different workloads, for scaling between 1 and
232+
2 threads. We'll admit that we used 75% read-write just because it emphasised
233+
the differences.
186234

187235
![](https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/implementation-write-proportion-scalability.png)
188236

189-
Finally, we can also run on a larger machine. We repeated the experiment using a machine with 64 physical cores and JRuby.
237+
Finally, we can also run on a larger machine. We repeated the experiment using a
238+
machine with 64 physical cores and JRuby.
190239

191240
![](https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/implementation-scalability.png)
192241

193242
![](https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/implementation-absolute.png)
194243

195-
Here you can see that `TVar` does become absolutely faster than using a global lock, at the slightly ridiculously thread-count of 50. It's probably not statistically significant anyway.
244+
Here you can see that `TVar` does become absolutely faster than using a global
245+
lock, at the slightly ridiculously thread-count of 50. It's probably not
246+
statistically significant anyway.

lib/concurrent/agent.rb

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
require 'concurrent/dereferenceable'
44
require 'concurrent/observable'
5-
require 'concurrent/utility/timeout'
65
require 'concurrent/logging'
76

87
module Concurrent
@@ -64,6 +63,7 @@ def rescue(clazz = StandardError, &block)
6463
end
6564
self
6665
end
66+
6767
alias_method :catch, :rescue
6868
alias_method :on_error, :rescue
6969

@@ -87,6 +87,7 @@ def validate(&block)
8787
end
8888
self
8989
end
90+
9091
alias_method :validates, :validate
9192
alias_method :validate_with, :validate
9293
alias_method :validates_with, :validate
@@ -106,20 +107,30 @@ def post(&block)
106107
# Update the current value with the result of the given block fast,
107108
# block can do blocking calls
108109
#
109-
# @param [Fixnum, nil] timeout maximum number of seconds before an update is cancelled
110+
# @param [Fixnum, nil] timeout [DEPRECATED] maximum number of seconds before an update is cancelled
110111
#
111112
# @yield the fast to be performed with the current value in order to calculate
112113
# the new value
113114
# @yieldparam [Object] value the current value
114115
# @yieldreturn [Object] the new value
115116
# @return [true, nil] nil when no block is given
116117
def post_off(timeout = nil, &block)
117-
block = if timeout
118-
lambda { |value| Concurrent::timeout(timeout) { block.call(value) } }
118+
warn '[DEPRECATED] post_off with timeout options is deprecated and will be removed'
119+
task = if timeout
120+
lambda do |value|
121+
future = Future.execute do
122+
block.call(value)
123+
end
124+
if future.wait(timeout)
125+
future.value!
126+
else
127+
raise Concurrent::TimeoutError
128+
end
129+
end
119130
else
120131
block
121132
end
122-
post_on(@io_executor, &block)
133+
post_on(@io_executor, &task)
123134
end
124135

125136
# Update the current value with the result of the given block fast,

lib/concurrent/tvar.rb

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -153,37 +153,36 @@ class Transaction
153153
ABORTED = Object.new
154154

155155
ReadLogEntry = Struct.new(:tvar, :version)
156-
UndoLogEntry = Struct.new(:tvar, :value)
157156

158157
AbortError = Class.new(StandardError)
159158

160159
def initialize
161-
@write_set = Set.new
162160
@read_log = []
163-
@undo_log = []
161+
@write_log = {}
164162
end
165163

166164
def read(tvar)
167165
Concurrent::abort_transaction unless valid?
168-
@read_log.push(ReadLogEntry.new(tvar, tvar.unsafe_version))
169-
tvar.unsafe_value
166+
167+
if @write_log.has_key? tvar
168+
@write_log[tvar]
169+
else
170+
@read_log.push(ReadLogEntry.new(tvar, tvar.unsafe_version))
171+
tvar.unsafe_value
172+
end
170173
end
171174

172175
def write(tvar, value)
173176
# Have we already written to this TVar?
174177

175-
unless @write_set.include? tvar
178+
unless @write_log.has_key? tvar
176179
# Try to lock the TVar
177180

178181
unless tvar.unsafe_lock.try_lock
179182
# Someone else is writing to this TVar - abort
180183
Concurrent::abort_transaction
181184
end
182185

183-
# We've locked it - add it to the write set
184-
185-
@write_set.add(tvar)
186-
187186
# If we previously wrote to it, check the version hasn't changed
188187

189188
@read_log.each do |log_entry|
@@ -193,27 +192,20 @@ def write(tvar, value)
193192
end
194193
end
195194

196-
# Record the current value of the TVar so we can undo it later
195+
# Record the value written
197196

198-
@undo_log.push(UndoLogEntry.new(tvar, tvar.unsafe_value))
199-
200-
# Write the new value to the TVar
201-
202-
tvar.unsafe_value = value
197+
@write_log[tvar] = value
203198
end
204199

205200
def abort
206-
@undo_log.each do |entry|
207-
entry.tvar.unsafe_value = entry.value
208-
end
209-
210201
unlock
211202
end
212203

213204
def commit
214205
return false unless valid?
215206

216-
@write_set.each do |tvar|
207+
@write_log.each_pair do |tvar, value|
208+
tvar.unsafe_value = value
217209
tvar.unsafe_increment_version
218210
end
219211

@@ -224,7 +216,7 @@ def commit
224216

225217
def valid?
226218
@read_log.each do |log_entry|
227-
unless @write_set.include? log_entry.tvar
219+
unless @write_log.has_key? log_entry.tvar
228220
if log_entry.tvar.unsafe_version > log_entry.version
229221
return false
230222
end
@@ -235,7 +227,7 @@ def valid?
235227
end
236228

237229
def unlock
238-
@write_set.each do |tvar|
230+
@write_log.each_key do |tvar|
239231
tvar.unsafe_lock.unlock
240232
end
241233
end

lib/concurrent/utility/timeout.rb

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55

66
module Concurrent
77

8-
# Wait the given number of seconds for the block operation to complete.
8+
# [DEPRECATED] Wait the given number of seconds for the block operation to complete.
99
# Intended to be a simpler and more reliable replacement to the Ruby
10-
# standard library `Timeout::timeout` method.
10+
# standard library `Timeout::timeout` method. It does not kill the task
11+
# so it finishes anyway. Advantage is that it cannot cause any ugly errors by
12+
# killing threads.
1113
#
1214
# @param [Integer] seconds The number of seconds to wait
13-
#
1415
# @return [Object] The result of the block operation
1516
#
1617
# @raise [Concurrent::TimeoutError] when the block operation does not complete
@@ -19,20 +20,16 @@ module Concurrent
1920
# @see http://ruby-doc.org/stdlib-2.2.0/libdoc/timeout/rdoc/Timeout.html Ruby Timeout::timeout
2021
#
2122
# @!macro monotonic_clock_warning
22-
def timeout(seconds)
23-
24-
thread = Thread.new do
25-
Thread.current[:result] = yield
26-
end
27-
success = thread.join(seconds)
23+
def timeout(seconds, &block)
24+
warn '[DEPRECATED] timeout is deprecated and will be removed'
2825

29-
if success
30-
return thread[:result]
26+
future = Future.execute(&block)
27+
future.wait(seconds)
28+
if future.complete?
29+
future.value!
3130
else
3231
raise TimeoutError
3332
end
34-
ensure
35-
Thread.kill(thread) unless thread.nil?
3633
end
3734
module_function :timeout
3835
end

0 commit comments

Comments
 (0)