Skip to content

Commit 550592d

Browse files
committed
Added Lazy, a simpler and faster variation of Delay.
1 parent 09475b4 commit 550592d

File tree

9 files changed

+211
-25
lines changed

9 files changed

+211
-25
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
* All high-level abstractions default to the "io executor"
3333
* Added shutdown/kill/wait_for_termination variants for global executors
3434
* Fixed bug in `Actor` causing it to prematurely warm global thread pools on gem load
35+
* Added `Lazy`, a simpler and faster varition of `Delay`
36+
- Updated most internal uses of `Delay` with `Lazy`
3537

3638
## Current Release v0.8.0 (25 January 2015)
3739

examples/lazy_and_delay.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env ruby
2+
3+
$: << File.expand_path('../../lib', __FILE__)
4+
5+
require 'benchmark'
6+
7+
require 'concurrent/delay'
8+
9+
n = 500_000
10+
11+
delay = Concurrent::Delay.new{ nil }
12+
lazy = Concurrent::Lazy.new{ nil }
13+
14+
delay.value
15+
lazy.value
16+
17+
Benchmark.bm do |x|
18+
puts 'Benchmarking Delay...'
19+
x.report { n.times{ delay.value } }
20+
puts 'Benchmarking Lazy...'
21+
x.report { n.times{ lazy.value } }
22+
end

lib/concurrent/actor.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'concurrent/configuration'
2+
require 'concurrent/delay'
23
require 'concurrent/executor/serialized_execution'
34
require 'concurrent/ivar'
45
require 'concurrent/logging'

lib/concurrent/async.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'thread'
22
require 'concurrent/configuration'
33
require 'concurrent/delay'
4+
require 'concurrent/lazy'
45
require 'concurrent/errors'
56
require 'concurrent/ivar'
67
require 'concurrent/executor/immediate_executor'
@@ -201,7 +202,7 @@ def init_mutex
201202
}
202203

203204
@__await_delegator__ = Delay.new(executor: :immediate) {
204-
AsyncDelegator.new(self, Delay.new{ Concurrent::ImmediateExecutor.new }, serializer, true)
205+
AsyncDelegator.new(self, Lazy.new{ Concurrent::ImmediateExecutor.new }, serializer, true)
205206
}
206207

207208
@__async_delegator__ = Delay.new(executor: :immediate) {

lib/concurrent/configuration.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
require 'thread'
2-
require 'concurrent/delay'
2+
require 'concurrent/lazy'
33
require 'concurrent/atomics'
44
require 'concurrent/errors'
55
require 'concurrent/executors'
@@ -12,17 +12,17 @@ module Concurrent
1212
class << self
1313
@@auto_terminate_global_executors = Concurrent::AtomicBoolean.new(true)
1414

15-
@@global_fast_executor = Delay.new(executor: :immediate) do
15+
@@global_fast_executor = Lazy.new do
1616
Concurrent.new_fast_executor(
1717
stop_on_exit: @@auto_terminate_global_executors.value)
1818
end
1919

20-
@@global_io_executor = Delay.new(executor: :immediate) do
20+
@@global_io_executor = Lazy.new do
2121
Concurrent.new_io_executor(
2222
stop_on_exit: @@auto_terminate_global_executors.value)
2323
end
2424

25-
@@global_timer_set = Delay.new(executor: :immediate) do
25+
@@global_timer_set = Lazy.new do
2626
Concurrent::TimerSet.new(
2727
stop_on_exit: @@auto_terminate_global_executors.value)
2828
end

lib/concurrent/delay.rb

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55

66
module Concurrent
77

8-
# Lazy evaluation of a block yielding an immutable result. Useful for expensive
9-
# operations that may never be needed.
10-
#
11-
# A `Delay` is similar to `Future` but solves a different problem.
12-
# Where a `Future` schedules an operation for immediate execution and
13-
# performs the operation asynchronously, a `Delay` (as the name implies)
14-
# delays execution of the operation until the result is actually needed.
8+
# Lazy evaluation of a block yielding an immutable result. Useful for
9+
# expensive operations that may never be needed. `Delay` is a more
10+
# complex and feature-rich version of `Lazy`. It is non-blocking,
11+
# supports the `Obligation` interface, and accepts the injection of
12+
# custom executor upon which to execute the block. Processing of
13+
# block will be deferred until the first time `#value` is called.
14+
# At that time the caller can choose to return immediately and let
15+
# the block execute asynchronously, block indefinitely, or block
16+
# with a timeout.
1517
#
1618
# When a `Delay` is created its state is set to `pending`. The value and
1719
# reason are both `nil`. The first time the `#value` method is called the
@@ -26,10 +28,16 @@ module Concurrent
2628
# `Delay` includes the `Concurrent::Dereferenceable` mixin to support thread
2729
# safety of the reference returned by `#value`.
2830
#
29-
# @see Concurrent::Dereferenceable
31+
# Because of its simplicity `Lazy` is much faster than `Delay`:
3032
#
31-
# @see http://clojuredocs.org/clojure_core/clojure.core/delay
32-
# @see http://aphyr.com/posts/306-clojure-from-the-ground-up-state
33+
# user system total real
34+
# Benchmarking Delay...
35+
# 0.730000 0.000000 0.730000 ( 0.738434)
36+
# Benchmarking Lazy...
37+
# 0.040000 0.000000 0.040000 ( 0.042322)
38+
#
39+
# @see Concurrent::Dereferenceable
40+
# @see Concurrent::Lazy
3341
class Delay
3442
include Obligation
3543
include ExecutorOptions
@@ -63,12 +71,20 @@ def initialize(opts = {}, &block)
6371
@computing = false
6472
end
6573

66-
def wait(timeout)
74+
# Return the value this object represents after applying the options
75+
# specified by the `#set_deref_options` method.
76+
#
77+
# @param [Integer] timeout (nil) the maximum number of seconds to wait for
78+
# the value to be computed. When `nil` the caller will block indefinitely.
79+
#
80+
# @return [Object] the current value of the object
81+
def wait(timeout = nil)
6782
execute_task_once
68-
super timeout
83+
super(timeout)
6984
end
7085

71-
# reconfigures the block returning the value if still #incomplete?
86+
# Reconfigures the block returning the value if still `#incomplete?`
87+
#
7288
# @yield the delayed operation to perform
7389
# @return [true, false] if success
7490
def reconfigure(&block)
@@ -86,7 +102,8 @@ def reconfigure(&block)
86102

87103
private
88104

89-
def execute_task_once
105+
# @!visibility private
106+
def execute_task_once # :nodoc:
90107
mutex.lock
91108
execute = @computing = true unless @computing
92109
task = @task

lib/concurrent/lazy.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
module Concurrent
2+
3+
# Lazy evaluation of a block yielding an immutable result. Useful for
4+
# expensive operations that may never be needed. `Lazy` is a simpler,
5+
# blocking version of `Delay` and has an API similar to `AtomicReference`.
6+
# The first time `#value` is called the caller will block until the
7+
# block given at construction is executed. Once the result has been
8+
# computed the value will be immutably set. Any exceptions thrown during
9+
# computation will be suppressed.
10+
#
11+
# Because of its simplicity `Lazy` is much faster than `Delay`:
12+
#
13+
# user system total real
14+
# Benchmarking Delay...
15+
# 0.730000 0.000000 0.730000 ( 0.738434)
16+
# Benchmarking Lazy...
17+
# 0.040000 0.000000 0.040000 ( 0.042322)
18+
#
19+
# @see Concurrent::Delay
20+
class Lazy
21+
22+
# Creates anew unfulfilled object.
23+
#
24+
# @yield the delayed operation to perform
25+
# @param [Object] default (nil) the default value for the object when
26+
# the block raises an exception
27+
#
28+
# @raise [ArgumentError] if no block is given
29+
def initialize(default = nil, &block)
30+
raise ArgumentError.new('no block given') unless block_given?
31+
@default = default
32+
@task = block
33+
@mutex = Mutex.new
34+
@value = nil
35+
@fulfilled = false
36+
end
37+
38+
# The calculated value of the object or the default value if one
39+
# was given at construction. This first time this method is called
40+
# it will block indefinitely while the block is processed.
41+
# Subsequent calls will not block.
42+
#
43+
# @return [Object] the calculated value
44+
def value
45+
# double-checked locking is safe because we only update once
46+
return @value if @fulfilled
47+
48+
@mutex.synchronize do
49+
unless @fulfilled
50+
begin
51+
@value = @task.call
52+
rescue
53+
@value = @default
54+
ensure
55+
@fulfilled = true
56+
end
57+
end
58+
return @value
59+
end
60+
end
61+
end
62+
end

lib/concurrent/utility/processor_count.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
require 'rbconfig'
2-
require 'concurrent/delay'
3-
require 'concurrent/executor/immediate_executor'
2+
require 'concurrent/lazy'
43

54
module Concurrent
65

76
class ProcessorCounter
87
def initialize
9-
@processor_count = Delay.new(executor: :immediate) { compute_processor_count }
10-
@physical_processor_count = Delay.new(executor: :immediate) { compute_physical_processor_count }
8+
@processor_count = Lazy.new { compute_processor_count }
9+
@physical_processor_count = Lazy.new { compute_physical_processor_count }
1110
end
1211

1312
# Number of processors seen by the OS and used for process scheduling. For
@@ -79,7 +78,7 @@ def compute_processor_count
7978
if os_name =~ /mingw|mswin/
8079
require 'win32ole'
8180
result = WIN32OLE.connect("winmgmts://").ExecQuery(
82-
"select NumberOfLogicalProcessors from Win32_Processor")
81+
"select NumberOfLogicalProcessors from Win32_Processor")
8382
result.to_enum.collect(&:NumberOfLogicalProcessors).reduce(:+)
8483
elsif File.readable?("/proc/cpuinfo")
8584
IO.read("/proc/cpuinfo").scan(/^processor/).size
@@ -128,7 +127,7 @@ def compute_physical_processor_count
128127
when /mswin|mingw/
129128
require 'win32ole'
130129
result_set = WIN32OLE.connect("winmgmts://").ExecQuery(
131-
"select NumberOfCores from Win32_Processor")
130+
"select NumberOfCores from Win32_Processor")
132131
result_set.to_enum.collect(&:NumberOfCores).reduce(:+)
133132
else
134133
processor_count

spec/concurrent/lazy_spec.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
module Concurrent
2+
3+
describe Lazy do
4+
5+
context '#initialize' do
6+
7+
it 'raises an exception when no block given' do
8+
expect {
9+
Lazy.new
10+
}.to raise_error(ArgumentError)
11+
end
12+
end
13+
14+
context '#value' do
15+
16+
let(:task){ proc{ nil } }
17+
18+
it 'does not call the block before #value is called' do
19+
expect(task).to_not receive(:call).with(any_args)
20+
Lazy.new(&task)
21+
end
22+
23+
it 'calls the block when #value is called' do
24+
expect(task).to receive(:call).once.with(any_args).and_return(nil)
25+
Lazy.new(&task).value
26+
end
27+
28+
it 'only calls the block once no matter how often #value is called' do
29+
expect(task).to receive(:call).once.with(any_args).and_return(nil)
30+
lazy = Lazy.new(&task)
31+
5.times{ lazy.value }
32+
end
33+
34+
it 'does not lock the mutex once the block has been called' do
35+
mutex = Mutex.new
36+
allow(Mutex).to receive(:new).and_return(mutex)
37+
38+
lazy = Lazy.new(&task)
39+
lazy.value
40+
41+
expect(mutex).to_not receive(:synchronize).with(any_args)
42+
expect(mutex).to_not receive(:lock).with(any_args)
43+
expect(mutex).to_not receive(:try_lock).with(any_args)
44+
45+
5.times{ lazy.value }
46+
end
47+
48+
context 'on exception' do
49+
50+
it 'suppresses the error' do
51+
expect {
52+
Lazy.new{ raise StandardError }
53+
}.to_not raise_exception
54+
end
55+
56+
it 'sets the value to nil when no default is given' do
57+
lazy = Lazy.new{ raise StandardError }
58+
expect(lazy.value).to be_nil
59+
end
60+
61+
it 'sets the value appropriately when given a default' do
62+
lazy = Lazy.new(100){ raise StandardError }
63+
expect(lazy.value).to eq 100
64+
end
65+
66+
it 'does not try to call the block again' do
67+
mutex = Mutex.new
68+
allow(Mutex).to receive(:new).and_return(mutex)
69+
70+
lazy = Lazy.new{ raise StandardError }
71+
lazy.value
72+
73+
expect(mutex).to_not receive(:synchronize).with(any_args)
74+
expect(mutex).to_not receive(:lock).with(any_args)
75+
expect(mutex).to_not receive(:try_lock).with(any_args)
76+
77+
5.times{ lazy.value }
78+
end
79+
end
80+
end
81+
end
82+
end

0 commit comments

Comments
 (0)