@@ -61,7 +61,7 @@ def future(executor = :fast, &block)
61
61
62
62
# @return [Delay]
63
63
def delay ( executor = :fast , &block )
64
- Delay . new ( executor , &block )
64
+ Delay . new ( nil , executor , &block ) . future
65
65
end
66
66
67
67
alias_method :async , :future
@@ -144,21 +144,22 @@ module FutureHelpers
144
144
# @return [Future]
145
145
def join ( *futures )
146
146
countdown = Concurrent ::AtomicFixnum . new futures . size
147
- promise = Promise . new . add_blocked_by ( * futures ) # TODO add injectable executor
147
+ promise = ExternalPromise . new ( futures )
148
148
futures . each { |future | future . add_callback :join , countdown , promise , *futures }
149
149
promise . future
150
150
end
151
151
152
152
# @return [Future]
153
153
def execute ( executor = :fast , &block )
154
- promise = Promise . new ( executor )
154
+ promise = ExternalPromise . new ( [ ] , executor )
155
155
Next . executor ( executor ) . post { promise . evaluate_to &block }
156
156
promise . future
157
157
end
158
158
end
159
159
160
160
class Future < SynchronizedObject
161
161
extend FutureHelpers
162
+ extend Shortcuts
162
163
163
164
singleton_class . send :alias_method , :dataflow , :join
164
165
@@ -264,26 +265,47 @@ def exception(*args)
264
265
reason . exception ( *args )
265
266
end
266
267
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
268
272
269
273
# @yield [success, value, reason] of the parent
270
274
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
272
276
promise . future
273
277
end
274
278
275
279
# @yield [value] executed only on parent success
276
280
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
278
282
promise . future
279
283
end
280
284
281
285
# @yield [reason] executed only on parent failure
282
286
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
284
288
promise . future
285
289
end
286
290
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
+
287
309
# @yield [success, value, reason] executed async on `executor` when completed
288
310
# @return self
289
311
def on_completion ( executor = default_executor , &callback )
@@ -399,27 +421,15 @@ def with_promise(promise, &block)
399
421
end
400
422
401
423
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 } }
407
425
end
408
426
409
427
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 } }
415
429
end
416
430
417
431
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 } }
423
433
end
424
434
425
435
def with_async ( executor )
@@ -450,20 +460,20 @@ def callback_on_failure(callback)
450
460
callback . call reason if failed?
451
461
end
452
462
463
+ def conditioned_callback ( callback )
464
+ self . success? ? callback . call ( value ) : raise ( reason )
465
+ end
466
+
453
467
def call_callback ( method , *args )
454
468
self . send method , *args
455
469
end
456
470
end
457
471
458
472
class Promise < SynchronizedObject
459
473
# @api private
460
- def initialize ( executor_or_future = :fast )
474
+ def initialize ( executor = :fast )
461
475
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 )
467
477
468
478
synchronize do
469
479
@future = future
@@ -480,6 +490,42 @@ def blocked_by
480
490
synchronize { @blocked_by }
481
491
end
482
492
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
+
483
529
# Set the `IVar` to a value and wake or notify all threads waiting on it.
484
530
#
485
531
# @param [Object] value the value to store in the `IVar`
@@ -506,15 +552,6 @@ def try_fail(reason = StandardError.new)
506
552
!!complete ( false , nil , reason , false )
507
553
end
508
554
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
-
518
555
# @return [Future]
519
556
def evaluate_to ( &block )
520
557
success block . call
@@ -526,56 +563,59 @@ def evaluate_to(&block)
526
563
def evaluate_to! ( &block )
527
564
evaluate_to ( &block ) . no_error!
528
565
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
529
575
530
576
# @return [Future]
531
577
def connect_to ( future )
532
578
add_blocked_by future
533
579
future . add_callback :set_promise_on_completion , self
534
580
self . future
535
581
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
554
582
end
555
583
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 )
561
587
synchronize do
588
+ @task = task
562
589
@computing = false
563
- @task = block
564
590
end
591
+ add_blocked_by blocked_by_future if blocked_by_future
565
592
end
566
593
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
570
601
end
571
602
572
- # starts executing the value without blocking
573
- def touch
603
+ private
604
+
605
+ def execute_once
574
606
execute , task = synchronize do
575
607
[ ( @computing = true unless @computing ) , @task ]
576
608
end
577
609
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
579
619
self
580
620
end
581
621
end
@@ -610,7 +650,7 @@ def touch
610
650
future2 = future1 . then { |v | v + 1 } # will fail with 'boo' error, executed on default FAST_EXECUTOR
611
651
future3 = future1 . rescue { |err | err . message } # executed on default FAST_EXECUTOR
612
652
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
614
654
future6 = future5 . then ( &:capitalize ) # executes on IO_EXECUTOR because default was set to :io on future5
615
655
future7 = Future . join ( future0 , future3 )
616
656
@@ -642,8 +682,9 @@ def touch
642
682
puts '-- promise like tree'
643
683
644
684
# 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 )
647
688
648
689
# meaningful to_s and inspect defined for Future and Promise
649
690
puts head
@@ -652,12 +693,65 @@ def touch
652
693
# <#Concurrent::Next::Delay:7f89b4bccc68 pending [<#Concurrent::Next::Promise:7f89b4bccb00 pending>]]>
653
694
p head . callbacks
654
695
# [[: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
656
718
657
719
puts '-- bench'
658
720
require 'benchmark'
659
721
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 |
661
755
662
756
parents = [ RubySynchronizedObject , ( JavaSynchronizedObject if defined? JavaSynchronizedObject ) ] . compact
663
757
classes = parents . map do |parent |
0 commit comments