Skip to content

Commit 3b877db

Browse files
committed
Allows to build promise trees with lazy evaluated branches
- Adds #chain_delay, #then_delay, #rescue_delay which are same as #chain, #then, #rescue but are not evaluated automatically but only when requested by #value. - Restructure class hierarchy. Only one Future with Multiple Promise implementations which are hidden to the user. Provides better encapsulation. - Delay is now implemented as a Promise descendant.
1 parent 16783f3 commit 3b877db

File tree

1 file changed

+166
-72
lines changed

1 file changed

+166
-72
lines changed

lib/concurrent/next.rb

Lines changed: 166 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def future(executor = :fast, &block)
6161

6262
# @return [Delay]
6363
def delay(executor = :fast, &block)
64-
Delay.new(executor, &block)
64+
Delay.new(nil, executor, &block).future
6565
end
6666

6767
alias_method :async, :future
@@ -144,21 +144,22 @@ module FutureHelpers
144144
# @return [Future]
145145
def join(*futures)
146146
countdown = Concurrent::AtomicFixnum.new futures.size
147-
promise = Promise.new.add_blocked_by(*futures) # TODO add injectable executor
147+
promise = ExternalPromise.new(futures)
148148
futures.each { |future| future.add_callback :join, countdown, promise, *futures }
149149
promise.future
150150
end
151151

152152
# @return [Future]
153153
def execute(executor = :fast, &block)
154-
promise = Promise.new(executor)
154+
promise = ExternalPromise.new([], executor)
155155
Next.executor(executor).post { promise.evaluate_to &block }
156156
promise.future
157157
end
158158
end
159159

160160
class Future < SynchronizedObject
161161
extend FutureHelpers
162+
extend Shortcuts
162163

163164
singleton_class.send :alias_method, :dataflow, :join
164165

@@ -264,26 +265,47 @@ def exception(*args)
264265
reason.exception(*args)
265266
end
266267

267-
# TODO add #then_delay { ... } and such to be able to chain delayed evaluations
268+
# TODO needs better name
269+
def connect(executor = default_executor)
270+
ConnectedPromise.new(self, executor).future
271+
end
268272

269273
# @yield [success, value, reason] of the parent
270274
def chain(executor = default_executor, &callback)
271-
add_callback :chain_callback, executor, promise = Promise.new(default_executor).add_blocked_by(self), callback
275+
add_callback :chain_callback, executor, promise = ExternalPromise.new([self], default_executor), callback
272276
promise.future
273277
end
274278

275279
# @yield [value] executed only on parent success
276280
def then(executor = default_executor, &callback)
277-
add_callback :then_callback, executor, promise = Promise.new(default_executor).add_blocked_by(self), callback
281+
add_callback :then_callback, executor, promise = ExternalPromise.new([self], default_executor), callback
278282
promise.future
279283
end
280284

281285
# @yield [reason] executed only on parent failure
282286
def rescue(executor = default_executor, &callback)
283-
add_callback :rescue_callback, executor, promise = Promise.new(default_executor).add_blocked_by(self), callback
287+
add_callback :rescue_callback, executor, promise = ExternalPromise.new([self], default_executor), callback
284288
promise.future
285289
end
286290

291+
# lazy version of #chain
292+
def chain_delay(executor = default_executor, &callback)
293+
delay = Delay.new(self, executor) { callback_on_completion callback }
294+
delay.future
295+
end
296+
297+
# lazy version of #then
298+
def then_delay(executor = default_executor, &callback)
299+
delay = Delay.new(self, executor) { conditioned_callback callback }
300+
delay.future
301+
end
302+
303+
# lazy version of #rescue
304+
def rescue_delay(executor = default_executor, &callback)
305+
delay = Delay.new(self, executor) { callback_on_failure callback }
306+
delay.future
307+
end
308+
287309
# @yield [success, value, reason] executed async on `executor` when completed
288310
# @return self
289311
def on_completion(executor = default_executor, &callback)
@@ -399,27 +421,15 @@ def with_promise(promise, &block)
399421
end
400422

401423
def chain_callback(executor, promise, callback)
402-
with_async(executor) do
403-
with_promise(promise) do
404-
callback_on_completion callback
405-
end
406-
end
424+
with_async(executor) { with_promise(promise) { callback_on_completion callback } }
407425
end
408426

409427
def then_callback(executor, promise, callback)
410-
with_async(executor) do
411-
with_promise(promise) do
412-
success? ? callback.call(value) : raise(reason)
413-
end
414-
end
428+
with_async(executor) { with_promise(promise) { conditioned_callback callback } }
415429
end
416430

417431
def rescue_callback(executor, promise, callback)
418-
with_async(executor) do
419-
with_promise(promise) do
420-
callback_on_failure callback
421-
end
422-
end
432+
with_async(executor) { with_promise(promise) { callback_on_failure callback } }
423433
end
424434

425435
def with_async(executor)
@@ -450,20 +460,20 @@ def callback_on_failure(callback)
450460
callback.call reason if failed?
451461
end
452462

463+
def conditioned_callback(callback)
464+
self.success? ? callback.call(value) : raise(reason)
465+
end
466+
453467
def call_callback(method, *args)
454468
self.send method, *args
455469
end
456470
end
457471

458472
class Promise < SynchronizedObject
459473
# @api private
460-
def initialize(executor_or_future = :fast)
474+
def initialize(executor = :fast)
461475
super()
462-
future = if Future === executor_or_future
463-
executor_or_future
464-
else
465-
Future.new(self, executor_or_future)
466-
end
476+
future = Future.new(self, executor)
467477

468478
synchronize do
469479
@future = future
@@ -480,6 +490,42 @@ def blocked_by
480490
synchronize { @blocked_by }
481491
end
482492

493+
def state
494+
future.state
495+
end
496+
497+
def touch
498+
blocked_by.each(&:touch) if synchronize { @touched ? false : (@touched = true) }
499+
end
500+
501+
def to_s
502+
"<##{self.class}:0x#{'%x' % (object_id << 1)} #{state}>"
503+
end
504+
505+
def inspect
506+
"#{to_s[0..-2]} blocked_by:[#{synchronize { @blocked_by }.map(&:to_s).join(', ')}]>"
507+
end
508+
509+
# @api private
510+
def complete(success, value, reason, raise = true)
511+
future.complete(success, value, reason, raise)
512+
synchronize { @blocked_by.clear }
513+
end
514+
515+
private
516+
517+
def add_blocked_by(*futures)
518+
synchronize { @blocked_by += futures }
519+
self
520+
end
521+
end
522+
523+
class ExternalPromise < Promise
524+
def initialize(blocked_by_futures, executor_or_future = :fast)
525+
super executor_or_future
526+
add_blocked_by *blocked_by_futures
527+
end
528+
483529
# Set the `IVar` to a value and wake or notify all threads waiting on it.
484530
#
485531
# @param [Object] value the value to store in the `IVar`
@@ -506,15 +552,6 @@ def try_fail(reason = StandardError.new)
506552
!!complete(false, nil, reason, false)
507553
end
508554

509-
def complete(success, value, reason, raise = true)
510-
future.complete(success, value, reason, raise)
511-
synchronize { @blocked_by.clear }
512-
end
513-
514-
def state
515-
future.state
516-
end
517-
518555
# @return [Future]
519556
def evaluate_to(&block)
520557
success block.call
@@ -526,56 +563,59 @@ def evaluate_to(&block)
526563
def evaluate_to!(&block)
527564
evaluate_to(&block).no_error!
528565
end
566+
end
567+
568+
class ConnectedPromise < Promise
569+
def initialize(future, executor_or_future = :fast)
570+
super(executor_or_future)
571+
connect_to future
572+
end
573+
574+
private
529575

530576
# @return [Future]
531577
def connect_to(future)
532578
add_blocked_by future
533579
future.add_callback :set_promise_on_completion, self
534580
self.future
535581
end
536-
537-
def touch
538-
blocked_by.each(&:touch) if synchronize { @touched ? false : (@touched = true) }
539-
end
540-
541-
def to_s
542-
"<##{self.class}:0x#{'%x' % (object_id << 1)} #{state}>"
543-
end
544-
545-
def inspect
546-
"#{to_s[0..-2]} blocked_by:[#{synchronize { @blocked_by }.map(&:to_s).join(', ')}]>"
547-
end
548-
549-
# @api private
550-
def add_blocked_by(*futures)
551-
synchronize { @blocked_by += futures }
552-
self
553-
end
554582
end
555583

556-
class Delay < Future
557-
558-
def initialize(default_executor = :fast, &block)
559-
super(Promise.new(self), default_executor)
560-
raise ArgumentError.new('no block given') unless block_given?
584+
class Delay < Promise
585+
def initialize(blocked_by_future, executor_or_future = :fast, &task)
586+
super(executor_or_future)
561587
synchronize do
588+
@task = task
562589
@computing = false
563-
@task = block
564590
end
591+
add_blocked_by blocked_by_future if blocked_by_future
565592
end
566593

567-
def wait(timeout = nil)
568-
touch
569-
super timeout
594+
def touch
595+
if blocked_by.all?(&:completed?)
596+
execute_once
597+
else
598+
blocked_by.each { |f| f.on_success! { self.touch } unless synchronize { @touched } }
599+
super
600+
end
570601
end
571602

572-
# starts executing the value without blocking
573-
def touch
603+
private
604+
605+
def execute_once
574606
execute, task = synchronize do
575607
[(@computing = true unless @computing), @task]
576608
end
577609

578-
Next.executor(default_executor).post { promise.evaluate_to &task } if execute
610+
if execute
611+
Next.executor(future.default_executor).post do
612+
begin
613+
complete true, task.call, nil
614+
rescue => error
615+
complete false, nil, error
616+
end
617+
end
618+
end
579619
self
580620
end
581621
end
@@ -610,7 +650,7 @@ def touch
610650
future2 = future1.then { |v| v + 1 } # will fail with 'boo' error, executed on default FAST_EXECUTOR
611651
future3 = future1.rescue { |err| err.message } # executed on default FAST_EXECUTOR
612652
future4 = future0.chain { |success, value, reason| success } # executed on default FAST_EXECUTOR
613-
future5 = Promise.new(:io).connect_to(future3)
653+
future5 = future3.connect(:io) # connects new future with different executor, the new future is completed when future3 is
614654
future6 = future5.then(&:capitalize) # executes on IO_EXECUTOR because default was set to :io on future5
615655
future7 = Future.join(future0, future3)
616656

@@ -642,8 +682,9 @@ def touch
642682
puts '-- promise like tree'
643683

644684
# if head of the tree is not constructed with #future but with #delay it does not start execute,
645-
# it's triggered later by calling wait or value on any of the depedent futures or the delay itself
646-
tree = (head = delay { 1 }).then { |v| v.succ }.then(&:succ).then(&:succ)
685+
# it's triggered later by calling wait or value on any of the dependent futures or the delay itself
686+
three = (head = delay { 1 }).then { |v| v.succ }.then(&:succ)
687+
four = three.then_delay(&:succ)
647688

648689
# meaningful to_s and inspect defined for Future and Promise
649690
puts head
@@ -652,12 +693,65 @@ def touch
652693
# <#Concurrent::Next::Delay:7f89b4bccc68 pending [<#Concurrent::Next::Promise:7f89b4bccb00 pending>]]>
653694
p head.callbacks
654695
# [[:then_callback, :fast, <#Concurrent::Next::Promise:0x7fa54b31d218 pending [<#Concurrent::Next::Delay:0x7fa54b31d380 pending>]>, #<Proc:0x007fa54b31d290>]]
655-
p tree.value
696+
697+
# evaluates only up to three, four is left unevaluated
698+
p three.value # 3
699+
p four, four.promise
700+
# until value is called on four
701+
p four.value # 4
702+
703+
# futures hidden behind two delays trigger evaluation of both
704+
double_delay = delay { 1 }.then_delay(&:succ)
705+
p double_delay.value # 2
706+
707+
puts '-- graph'
708+
709+
head = future { 1 }
710+
branch1 = head.then(&:succ).then(&:succ)
711+
branch2 = head.then(&:succ).then_delay(&:succ)
712+
result = Future.join(branch1, branch2).then { |b1, b2| b1 + b2 }
713+
714+
sleep 0.1
715+
p branch1.completed?, branch2.completed? # true, false
716+
# force evaluation of whole graph
717+
p result.value # 6
656718

657719
puts '-- bench'
658720
require 'benchmark'
659721

660-
Benchmark.bmbm(20) do |b|
722+
module Benchmark
723+
def self.bmbmbm(rehearsals, width)
724+
job = Job.new(width)
725+
yield(job)
726+
width = job.width + 1
727+
sync = STDOUT.sync
728+
STDOUT.sync = true
729+
730+
# rehearsal
731+
rehearsals.times do
732+
puts 'Rehearsal '.ljust(width+CAPTION.length, '-')
733+
ets = job.list.inject(Tms.new) { |sum, (label, item)|
734+
print label.ljust(width)
735+
res = Benchmark.measure(&item)
736+
print res.format
737+
sum + res
738+
}.format("total: %tsec")
739+
print " #{ets}\n\n".rjust(width+CAPTION.length+2, '-')
740+
end
741+
742+
# take
743+
print ' '*width + CAPTION
744+
job.list.map { |label, item|
745+
GC.start
746+
print label.ljust(width)
747+
Benchmark.measure(label, &item).tap { |res| print res }
748+
}
749+
ensure
750+
STDOUT.sync = sync unless sync.nil?
751+
end
752+
end
753+
754+
Benchmark.bmbmbm(20, 20) do |b|
661755

662756
parents = [RubySynchronizedObject, (JavaSynchronizedObject if defined? JavaSynchronizedObject)].compact
663757
classes = parents.map do |parent|

0 commit comments

Comments
 (0)