async 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +0 -1
- data/Rakefile +1 -1
- data/benchmark/async_vs_lightio.rb +59 -20
- data/examples/fibers.rb +178 -0
- data/lib/async/condition.rb +36 -4
- data/lib/async/notification.rb +50 -0
- data/lib/async/reactor.rb +22 -4
- data/lib/async/version.rb +1 -1
- data/spec/async/condition_examples.rb +53 -0
- data/spec/async/condition_spec.rb +4 -0
- data/spec/async/notification_spec.rb +63 -0
- metadata +8 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: c4f42d14b27e2c7835cd745cae1e3fed8cea3ab36e4e919676b38673fc8adf32
         | 
| 4 | 
            +
              data.tar.gz: 8944fd0464f0f1ef8135440bf4caa5a0235a0ef03de5e115609794c2a880ef76
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 3843d40191f22c646adfd596bb0360431dcca7152127fb90154932b430d2cbcee4e6a6479b804b34ebceef0b92535bf46d7c856a0c3aed6c3b9843c5973c2a09
         | 
| 7 | 
            +
              data.tar.gz: cc595c9163bf0819bdb5f2029685bc4839faa569f88cb6cb969662d89732f2a0c25da0f7b78f297eb061e4c54d68e50ae6a5ed079a13a781f846cfee47edcea6
         | 
    
        data/Gemfile
    CHANGED
    
    
    
        data/Rakefile
    CHANGED
    
    
| @@ -5,39 +5,78 @@ require 'lightio' | |
| 5 5 |  | 
| 6 6 | 
             
            require 'benchmark/ips'
         | 
| 7 7 |  | 
| 8 | 
            -
             | 
| 8 | 
            +
            #
         | 
| 9 | 
            +
            # It's hard to know exactly how to interpret these results. When running parallel
         | 
| 10 | 
            +
            # instances, resource contention is more likely to be a problem, and yet with
         | 
| 11 | 
            +
            # async, the performance between a single task and several tasks is roughly the
         | 
| 12 | 
            +
            # same, while in the case of lightio, there is an obvious performance gap.
         | 
| 13 | 
            +
            # 
         | 
| 14 | 
            +
            # The main takeaway is that contention causes issues and if systems are not
         | 
| 15 | 
            +
            # designed with that in mind, it will impact performance.
         | 
| 16 | 
            +
            #
         | 
| 17 | 
            +
            # $ ruby async_vs_lightio.rb
         | 
| 18 | 
            +
            # Warming up --------------------------------------
         | 
| 19 | 
            +
            # lightio (synchronous)
         | 
| 20 | 
            +
            #                          2.439k i/100ms
         | 
| 21 | 
            +
            #  async (synchronous)     2.115k i/100ms
         | 
| 22 | 
            +
            #   lightio (parallel)   211.000  i/100ms
         | 
| 23 | 
            +
            #     async (parallel)   449.000  i/100ms
         | 
| 24 | 
            +
            # Calculating -------------------------------------
         | 
| 25 | 
            +
            # lightio (synchronous)
         | 
| 26 | 
            +
            #                          64.502k (± 3.9%) i/s -    643.896k in  10.002151s
         | 
| 27 | 
            +
            #  async (synchronous)    161.195k (± 1.6%) i/s -      1.612M in  10.000976s
         | 
| 28 | 
            +
            #   lightio (parallel)     49.827k (±17.5%) i/s -    477.704k in   9.999579s
         | 
| 29 | 
            +
            #     async (parallel)    166.862k (± 6.2%) i/s -      1.662M in  10.000365s
         | 
| 30 | 
            +
            # 
         | 
| 31 | 
            +
            # Comparison:
         | 
| 32 | 
            +
            #     async (parallel):   166862.3 i/s
         | 
| 33 | 
            +
            #  async (synchronous):   161194.6 i/s - same-ish: difference falls within error
         | 
| 34 | 
            +
            # lightio (synchronous):   64502.5 i/s - 2.59x  slower
         | 
| 35 | 
            +
            #   lightio (parallel):    49827.3 i/s - 3.35x  slower
         | 
| 36 | 
            +
             | 
| 37 | 
            +
             | 
| 38 | 
            +
            DURATION = 0.000001
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            def run_async(count, repeats = 10000)
         | 
| 9 41 | 
             
            	Async::Reactor.run do |task|
         | 
| 10 | 
            -
            		 | 
| 11 | 
            -
            			# LightIO::Beam is a thread-like executor, use it instead Thread
         | 
| 42 | 
            +
            		count.times.map do
         | 
| 12 43 | 
             
            			task.async do |subtask|
         | 
| 13 | 
            -
            				 | 
| 14 | 
            -
             | 
| 44 | 
            +
            				repeats.times do
         | 
| 45 | 
            +
            					subtask.sleep(DURATION)
         | 
| 46 | 
            +
            				end
         | 
| 15 47 | 
             
            			end
         | 
| 16 | 
            -
            		end
         | 
| 17 | 
            -
            		
         | 
| 18 | 
            -
            		tasks.each(&:wait)
         | 
| 48 | 
            +
            		end.each(&:wait)
         | 
| 19 49 | 
             
            	end
         | 
| 20 50 | 
             
            end
         | 
| 21 51 |  | 
| 22 | 
            -
            def run_lightio(count = 10000)
         | 
| 23 | 
            -
            	 | 
| 24 | 
            -
            		# LightIO::Beam is a thread-like executor, use it instead Thread
         | 
| 52 | 
            +
            def run_lightio(count, repeats = 10000)
         | 
| 53 | 
            +
            	count.times.map do
         | 
| 25 54 | 
             
            		LightIO::Beam.new do
         | 
| 26 | 
            -
            			 | 
| 27 | 
            -
             | 
| 55 | 
            +
            			repeats.times do
         | 
| 56 | 
            +
            				LightIO.sleep(DURATION)
         | 
| 57 | 
            +
            			end
         | 
| 28 58 | 
             
            		end
         | 
| 29 | 
            -
            	end
         | 
| 30 | 
            -
             | 
| 31 | 
            -
            	beams.each(&:join)
         | 
| 59 | 
            +
            	end.each(&:join)
         | 
| 32 60 | 
             
            end
         | 
| 33 61 |  | 
| 34 62 | 
             
            Benchmark.ips do |benchmark|
         | 
| 35 | 
            -
            	benchmark. | 
| 36 | 
            -
             | 
| 63 | 
            +
            	benchmark.time = 10
         | 
| 64 | 
            +
            	benchmark.warmup = 2
         | 
| 65 | 
            +
            	
         | 
| 66 | 
            +
            	benchmark.report("lightio (synchronous)") do |count|
         | 
| 67 | 
            +
            		run_lightio(1, count)
         | 
| 68 | 
            +
            	end
         | 
| 69 | 
            +
            	
         | 
| 70 | 
            +
            	benchmark.report("async (synchronous)") do |count|
         | 
| 71 | 
            +
            		run_async(1, count)
         | 
| 72 | 
            +
            	end
         | 
| 73 | 
            +
            	
         | 
| 74 | 
            +
            	benchmark.report("lightio (parallel)") do |count|
         | 
| 75 | 
            +
            		run_lightio(32, count/32)
         | 
| 37 76 | 
             
            	end
         | 
| 38 77 |  | 
| 39 | 
            -
            	benchmark.report("async") do |count|
         | 
| 40 | 
            -
            		run_async(count)
         | 
| 78 | 
            +
            	benchmark.report("async (parallel)") do |count|
         | 
| 79 | 
            +
            		run_async(32, count/32)
         | 
| 41 80 | 
             
            	end
         | 
| 42 81 |  | 
| 43 82 | 
             
            	benchmark.compare!
         | 
    
        data/examples/fibers.rb
    ADDED
    
    | @@ -0,0 +1,178 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'fiber'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            class IO
         | 
| 6 | 
            +
            	READABLE = 1
         | 
| 7 | 
            +
            	WRITABLE = 2
         | 
| 8 | 
            +
            	
         | 
| 9 | 
            +
            	# rb_wait_for_single_fd (int fd, int events, struct timeval *tv)
         | 
| 10 | 
            +
            	def self.wait(descriptor, events, duration)
         | 
| 11 | 
            +
            		fiber = Fiber.current
         | 
| 12 | 
            +
            		reactor = fiber.reactor
         | 
| 13 | 
            +
            		
         | 
| 14 | 
            +
            		monitor = reactor.add_io(fiber, descriptor, state)
         | 
| 15 | 
            +
            		
         | 
| 16 | 
            +
            		fiber.timeout(duration) do
         | 
| 17 | 
            +
            			result = Fiber.yield
         | 
| 18 | 
            +
            			raise result if result.is_a? Exception
         | 
| 19 | 
            +
            		end
         | 
| 20 | 
            +
            		
         | 
| 21 | 
            +
            		return result
         | 
| 22 | 
            +
            	ensure
         | 
| 23 | 
            +
            		reactor.remove_io(monitor)
         | 
| 24 | 
            +
            	end
         | 
| 25 | 
            +
            	
         | 
| 26 | 
            +
            	def wait_readable(duration = nil)
         | 
| 27 | 
            +
            		wait_any(READABLE)
         | 
| 28 | 
            +
            	end
         | 
| 29 | 
            +
            	
         | 
| 30 | 
            +
            	def wait_writable(duration = nil)
         | 
| 31 | 
            +
            		wait_any(WRITABLE)
         | 
| 32 | 
            +
            	end
         | 
| 33 | 
            +
            	
         | 
| 34 | 
            +
            	def wait_until(events = READABLE|WRITABLE, duration = nil)
         | 
| 35 | 
            +
            		IO.wait_for_io(self.fileno, events, duration)
         | 
| 36 | 
            +
            	end
         | 
| 37 | 
            +
            end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            class Fiber
         | 
| 40 | 
            +
            	# Raised when a task times out.
         | 
| 41 | 
            +
            	class TimeoutError < RuntimeError
         | 
| 42 | 
            +
            	end
         | 
| 43 | 
            +
            	
         | 
| 44 | 
            +
            	# This should be inherited by nested fibers.
         | 
| 45 | 
            +
            	attr :reactor
         | 
| 46 | 
            +
            	
         | 
| 47 | 
            +
            	def timeout(duration)
         | 
| 48 | 
            +
            		reactor = self.reactor
         | 
| 49 | 
            +
            		backtrace = caller
         | 
| 50 | 
            +
            		
         | 
| 51 | 
            +
            		timer = reactor.add_timer(duration) do
         | 
| 52 | 
            +
            			if self.alive?
         | 
| 53 | 
            +
            				error = Fiber::TimeoutError.new("execution expired")
         | 
| 54 | 
            +
            				error.set_backtrace backtrace
         | 
| 55 | 
            +
            				self.resume error
         | 
| 56 | 
            +
            			end
         | 
| 57 | 
            +
            		end
         | 
| 58 | 
            +
            		
         | 
| 59 | 
            +
            		yield
         | 
| 60 | 
            +
            	ensure
         | 
| 61 | 
            +
            		reactor.cancel_timer(timer)
         | 
| 62 | 
            +
            	end
         | 
| 63 | 
            +
            end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            # Can be standard implementation, but could also be provided by external gem/library.
         | 
| 66 | 
            +
            class Fiber::Reactor
         | 
| 67 | 
            +
            	# Add IO to the reactor. The reactor will call `fiber.resume` when the event is triggered.
         | 
| 68 | 
            +
            	# Returns an opaque monitor object which can be passed to `remove_io` to stop waiting for events.
         | 
| 69 | 
            +
            	def add_io(fiber, io, state)
         | 
| 70 | 
            +
            		# The motivation for add_io and remove_io is that it's how a lot of the underlying APIs work, where remove_io just takes the file descriptor.
         | 
| 71 | 
            +
            		# It also avoids the need for any memory allocation, and maps well to how it's typically used (i.e. in an implementation of `IO#read`).
         | 
| 72 | 
            +
            		# An efficient implementation might do it's job and then just:
         | 
| 73 | 
            +
            		return io
         | 
| 74 | 
            +
            	end
         | 
| 75 | 
            +
            	
         | 
| 76 | 
            +
            	def remove_io(monitor)
         | 
| 77 | 
            +
            	end
         | 
| 78 | 
            +
            	
         | 
| 79 | 
            +
            	# The reactor will call the block at some point after duration time has elapsed.
         | 
| 80 | 
            +
            	# Returns an opaque timer object which can be passed to `cancel_timer` to avoid this happening.
         | 
| 81 | 
            +
            	def add_timer(duration, &block)
         | 
| 82 | 
            +
            	end
         | 
| 83 | 
            +
            	
         | 
| 84 | 
            +
            	def cancel_timer(timer)
         | 
| 85 | 
            +
            	end
         | 
| 86 | 
            +
            	
         | 
| 87 | 
            +
            	# Run until idle (no registered io/timers), or duration time has passed if specified.
         | 
| 88 | 
            +
            	def run(duration = nil)
         | 
| 89 | 
            +
            	end
         | 
| 90 | 
            +
            	
         | 
| 91 | 
            +
            	# Stop the reactor as soon as possible. Can be called from another thread.
         | 
| 92 | 
            +
            	def stop
         | 
| 93 | 
            +
            	end
         | 
| 94 | 
            +
            	
         | 
| 95 | 
            +
            	def close
         | 
| 96 | 
            +
            		# Close the reactor so it can no longer be used.
         | 
| 97 | 
            +
            	end
         | 
| 98 | 
            +
            end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
            # Basic non-blocking task:
         | 
| 101 | 
            +
            reactor = Fiber::Reactor.new
         | 
| 102 | 
            +
             | 
| 103 | 
            +
            # User could provide their own reactor, it might even do other things, but the basic interface above should continue to work.
         | 
| 104 | 
            +
             | 
| 105 | 
            +
            Fiber.new(reactor: reactor) do
         | 
| 106 | 
            +
            	# Blocking operations call Fiber.yield, which goes to...
         | 
| 107 | 
            +
            end.resume
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            # ...here, which starts running the reactor which can also be controlled (e.g. duration, stopping)
         | 
| 110 | 
            +
            reactor.run
         | 
| 111 | 
            +
             | 
| 112 | 
            +
            # Here is a rough outline of the reactor concept implementation using NIO4R
         | 
| 113 | 
            +
            # Can be standard implementation, but could also be provided by external gem/library.
         | 
| 114 | 
            +
            class NIO::Reactor
         | 
| 115 | 
            +
            	def initialize
         | 
| 116 | 
            +
            		@selector = NIO::Selector.new
         | 
| 117 | 
            +
            		@timers = Timers::Group.new
         | 
| 118 | 
            +
            		
         | 
| 119 | 
            +
            		@stopped = true
         | 
| 120 | 
            +
            	end
         | 
| 121 | 
            +
            	
         | 
| 122 | 
            +
            	EVENTS = [
         | 
| 123 | 
            +
            		:r,
         | 
| 124 | 
            +
            		:w,
         | 
| 125 | 
            +
            		:rw
         | 
| 126 | 
            +
            	]
         | 
| 127 | 
            +
            	
         | 
| 128 | 
            +
            	def add_io(fiber, io, event)
         | 
| 129 | 
            +
            		monitor = @selector.register(io, EVENTS[event])
         | 
| 130 | 
            +
            		monitor.value = fiber
         | 
| 131 | 
            +
            	end
         | 
| 132 | 
            +
            	
         | 
| 133 | 
            +
            	def remove_io(monitor)
         | 
| 134 | 
            +
            		monitor.cancel
         | 
| 135 | 
            +
            	end
         | 
| 136 | 
            +
            	
         | 
| 137 | 
            +
            	# The reactor will call `fiber.resume(Fiber::TimeoutError)` at some point after duration time has elapsed.
         | 
| 138 | 
            +
            	# Returns an opaque timer object which can be passed to `cancel_timer` to avoid this happening.
         | 
| 139 | 
            +
            	def add_timer(fiber, duration)
         | 
| 140 | 
            +
            		@timers.after(duration, &block)
         | 
| 141 | 
            +
            	end
         | 
| 142 | 
            +
            	
         | 
| 143 | 
            +
            	def cancel_timer(timer)
         | 
| 144 | 
            +
            		timer.cancel
         | 
| 145 | 
            +
            	end
         | 
| 146 | 
            +
            	
         | 
| 147 | 
            +
            	# Run until idle (no registered io/timers), or duration time has passed if specified.
         | 
| 148 | 
            +
            	def run(duration = nil)
         | 
| 149 | 
            +
            		@timers.wait do |interval|
         | 
| 150 | 
            +
            			# - nil: no timers
         | 
| 151 | 
            +
            			# - -ve: timers expired already
         | 
| 152 | 
            +
            			# -   0: timers ready to fire
         | 
| 153 | 
            +
            			# - +ve: timers waiting to fire
         | 
| 154 | 
            +
            			interval = 0 if interval && interval < 0
         | 
| 155 | 
            +
            			
         | 
| 156 | 
            +
            			# If there is nothing to do, then finish:
         | 
| 157 | 
            +
            			return if @fibers.empty? && interval.nil?
         | 
| 158 | 
            +
            			
         | 
| 159 | 
            +
            			if monitors = @selector.select(interval)
         | 
| 160 | 
            +
            				monitors.each do |monitor|
         | 
| 161 | 
            +
            					if fiber = monitor.value
         | 
| 162 | 
            +
            						fiber.resume
         | 
| 163 | 
            +
            					end
         | 
| 164 | 
            +
            				end
         | 
| 165 | 
            +
            			end
         | 
| 166 | 
            +
            		end until @stopped
         | 
| 167 | 
            +
            	end
         | 
| 168 | 
            +
            	
         | 
| 169 | 
            +
            	def stop
         | 
| 170 | 
            +
            		@stopped = true
         | 
| 171 | 
            +
            		@selector.wakeup
         | 
| 172 | 
            +
            	end
         | 
| 173 | 
            +
            	
         | 
| 174 | 
            +
            	def close
         | 
| 175 | 
            +
            		@seletor.close
         | 
| 176 | 
            +
            	end
         | 
| 177 | 
            +
            end
         | 
| 178 | 
            +
             | 
    
        data/lib/async/condition.rb
    CHANGED
    
    | @@ -23,7 +23,7 @@ require 'forwardable' | |
| 23 23 | 
             
            require_relative 'node'
         | 
| 24 24 |  | 
| 25 25 | 
             
            module Async
         | 
| 26 | 
            -
            	# A synchronization primative, which allows fibers to wait until a particular condition is triggered.
         | 
| 26 | 
            +
            	# A synchronization primative, which allows fibers to wait until a particular condition is triggered. Signalling the condition directly resumes the waiting fibers and thus blocks the caller.
         | 
| 27 27 | 
             
            	class Condition
         | 
| 28 28 | 
             
            		def initialize
         | 
| 29 29 | 
             
            			@waiting = []
         | 
| @@ -37,17 +37,49 @@ module Async | |
| 37 37 | 
             
            			Task.yield
         | 
| 38 38 | 
             
            		end
         | 
| 39 39 |  | 
| 40 | 
            +
            		# Is any fiber waiting on this notification?
         | 
| 41 | 
            +
            		# @return [Boolean]
         | 
| 42 | 
            +
            		def empty?
         | 
| 43 | 
            +
            			@waiting.empty?
         | 
| 44 | 
            +
            		end
         | 
| 45 | 
            +
            		
         | 
| 40 46 | 
             
            		# Signal to a given task that it should resume operations.
         | 
| 41 47 | 
             
            		# @param value The value to return to the waiting fibers.
         | 
| 42 48 | 
             
            		# @see Task.yield which is responsible for handling value.
         | 
| 43 49 | 
             
            		# @return [void]
         | 
| 44 50 | 
             
            		def signal(value = nil)
         | 
| 45 | 
            -
            			 | 
| 46 | 
            -
            			 | 
| 47 | 
            -
             | 
| 51 | 
            +
            			waiting = @waiting
         | 
| 52 | 
            +
            			@waiting = []
         | 
| 53 | 
            +
            			
         | 
| 54 | 
            +
            			waiting.each do |fiber|
         | 
| 55 | 
            +
            				fiber.resume(value) if fiber.alive?
         | 
| 48 56 | 
             
            			end
         | 
| 49 57 |  | 
| 50 58 | 
             
            			return nil
         | 
| 51 59 | 
             
            		end
         | 
| 60 | 
            +
            		
         | 
| 61 | 
            +
            		# Signal to a given task that it should resume operations.
         | 
| 62 | 
            +
            		# @return [void]
         | 
| 63 | 
            +
            		def resume(value = nil, task: Task.current)
         | 
| 64 | 
            +
            			return if @waiting.empty?
         | 
| 65 | 
            +
            			
         | 
| 66 | 
            +
            			task.reactor << Signal.new(@waiting, value)
         | 
| 67 | 
            +
            			
         | 
| 68 | 
            +
            			@waiting = []
         | 
| 69 | 
            +
            			
         | 
| 70 | 
            +
            			return nil
         | 
| 71 | 
            +
            		end
         | 
| 72 | 
            +
            		
         | 
| 73 | 
            +
            		Signal = Struct.new(:waiting, :value) do
         | 
| 74 | 
            +
            			def alive?
         | 
| 75 | 
            +
            				true
         | 
| 76 | 
            +
            			end
         | 
| 77 | 
            +
            			
         | 
| 78 | 
            +
            			def resume
         | 
| 79 | 
            +
            				waiting.each do |fiber|
         | 
| 80 | 
            +
            					fiber.resume(value) if fiber.alive?
         | 
| 81 | 
            +
            				end
         | 
| 82 | 
            +
            			end
         | 
| 83 | 
            +
            		end
         | 
| 52 84 | 
             
            	end
         | 
| 53 85 | 
             
            end
         | 
| @@ -0,0 +1,50 @@ | |
| 1 | 
            +
            # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
         | 
| 2 | 
            +
            # 
         | 
| 3 | 
            +
            # Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 4 | 
            +
            # of this software and associated documentation files (the "Software"), to deal
         | 
| 5 | 
            +
            # in the Software without restriction, including without limitation the rights
         | 
| 6 | 
            +
            # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 7 | 
            +
            # copies of the Software, and to permit persons to whom the Software is
         | 
| 8 | 
            +
            # furnished to do so, subject to the following conditions:
         | 
| 9 | 
            +
            # 
         | 
| 10 | 
            +
            # The above copyright notice and this permission notice shall be included in
         | 
| 11 | 
            +
            # all copies or substantial portions of the Software.
         | 
| 12 | 
            +
            # 
         | 
| 13 | 
            +
            # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 14 | 
            +
            # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 15 | 
            +
            # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 16 | 
            +
            # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 17 | 
            +
            # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 18 | 
            +
            # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 19 | 
            +
            # THE SOFTWARE.
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            require_relative 'condition'
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            module Async
         | 
| 24 | 
            +
            	# A synchronization primative, which allows fibers to wait until a notification is received. Does not block the task which signals the notification. Waiting tasks are resumed on next iteration of the reactor.
         | 
| 25 | 
            +
            	class Notification < Condition
         | 
| 26 | 
            +
            		# Signal to a given task that it should resume operations.
         | 
| 27 | 
            +
            		# @return [void]
         | 
| 28 | 
            +
            		def signal(value = nil, task: Task.current)
         | 
| 29 | 
            +
            			return if @waiting.empty?
         | 
| 30 | 
            +
            			
         | 
| 31 | 
            +
            			task.reactor << Signal.new(@waiting, value)
         | 
| 32 | 
            +
            			
         | 
| 33 | 
            +
            			@waiting = []
         | 
| 34 | 
            +
            			
         | 
| 35 | 
            +
            			return nil
         | 
| 36 | 
            +
            		end
         | 
| 37 | 
            +
            		
         | 
| 38 | 
            +
            		Signal = Struct.new(:waiting, :value) do
         | 
| 39 | 
            +
            			def alive?
         | 
| 40 | 
            +
            				true
         | 
| 41 | 
            +
            			end
         | 
| 42 | 
            +
            			
         | 
| 43 | 
            +
            			def resume
         | 
| 44 | 
            +
            				waiting.each do |fiber|
         | 
| 45 | 
            +
            					fiber.resume(value) if fiber.alive?
         | 
| 46 | 
            +
            				end
         | 
| 47 | 
            +
            			end
         | 
| 48 | 
            +
            		end
         | 
| 49 | 
            +
            	end
         | 
| 50 | 
            +
            end
         | 
    
        data/lib/async/reactor.rb
    CHANGED
    
    | @@ -64,6 +64,8 @@ module Async | |
| 64 64 | 
             
            			@selector = NIO::Selector.new
         | 
| 65 65 | 
             
            			@timers = Timers::Group.new
         | 
| 66 66 |  | 
| 67 | 
            +
            			@ready = []
         | 
| 68 | 
            +
            			
         | 
| 67 69 | 
             
            			@stopped = true
         | 
| 68 70 | 
             
            		end
         | 
| 69 71 |  | 
| @@ -116,7 +118,19 @@ module Async | |
| 116 118 | 
             
            				@selector.wakeup
         | 
| 117 119 | 
             
            			end
         | 
| 118 120 | 
             
            		end
         | 
| 119 | 
            -
             | 
| 121 | 
            +
            		
         | 
| 122 | 
            +
            		# Schedule a fiber (or equivalent object) to be resumed on the next loop through the reactor.
         | 
| 123 | 
            +
            		def << fiber
         | 
| 124 | 
            +
            			@ready << fiber
         | 
| 125 | 
            +
            		end
         | 
| 126 | 
            +
            		
         | 
| 127 | 
            +
            		# Yield the current fiber and resume it on the next iteration of the event loop.
         | 
| 128 | 
            +
            		def yield(fiber = Fiber.current)
         | 
| 129 | 
            +
            			@ready << fiber
         | 
| 130 | 
            +
            			
         | 
| 131 | 
            +
            			Fiber.yield
         | 
| 132 | 
            +
            		end
         | 
| 133 | 
            +
            		
         | 
| 120 134 | 
             
            		# Run the reactor until either all tasks complete or {#stop} is invoked.
         | 
| 121 135 | 
             
            		# Proxies arguments to {#async} immediately before entering the loop.
         | 
| 122 136 | 
             
            		def run(*args, &block)
         | 
| @@ -137,6 +151,10 @@ module Async | |
| 137 151 | 
             
            				# Async.logger.debug{"[#{self} Pre] Updating #{@children.count} children..."}
         | 
| 138 152 | 
             
            				# As timeouts may have been updated, and caused fibers to complete, we should check this.
         | 
| 139 153 |  | 
| 154 | 
            +
            				@ready.each do |fiber|
         | 
| 155 | 
            +
            					fiber.resume if fiber.alive?
         | 
| 156 | 
            +
            				end; @ready.clear
         | 
| 157 | 
            +
            				
         | 
| 140 158 | 
             
            				# If there is nothing to do, then finish:
         | 
| 141 159 | 
             
            				# Async.logger.debug{"[#{self}] @children.empty? = #{@children.empty?} && interval #{interval.inspect}"}
         | 
| 142 160 | 
             
            				return initial_task if @children.empty? && interval.nil?
         | 
| @@ -178,11 +196,11 @@ module Async | |
| 178 196 | 
             
            		# Put the calling fiber to sleep for a given ammount of time.
         | 
| 179 197 | 
             
            		# @param duration [Numeric] The time in seconds, to sleep for.
         | 
| 180 198 | 
             
            		def sleep(duration)
         | 
| 181 | 
            -
            			 | 
| 199 | 
            +
            			fiber = Fiber.current
         | 
| 182 200 |  | 
| 183 201 | 
             
            			timer = self.after(duration) do
         | 
| 184 | 
            -
            				if  | 
| 185 | 
            -
            					 | 
| 202 | 
            +
            				if fiber.alive?
         | 
| 203 | 
            +
            					fiber.resume
         | 
| 186 204 | 
             
            				end
         | 
| 187 205 | 
             
            			end
         | 
| 188 206 |  | 
    
        data/lib/async/version.rb
    CHANGED
    
    
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
         | 
| 2 | 
            +
            # 
         | 
| 3 | 
            +
            # Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 4 | 
            +
            # of this software and associated documentation files (the "Software"), to deal
         | 
| 5 | 
            +
            # in the Software without restriction, including without limitation the rights
         | 
| 6 | 
            +
            # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 7 | 
            +
            # copies of the Software, and to permit persons to whom the Software is
         | 
| 8 | 
            +
            # furnished to do so, subject to the following conditions:
         | 
| 9 | 
            +
            # 
         | 
| 10 | 
            +
            # The above copyright notice and this permission notice shall be included in
         | 
| 11 | 
            +
            # all copies or substantial portions of the Software.
         | 
| 12 | 
            +
            # 
         | 
| 13 | 
            +
            # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 14 | 
            +
            # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 15 | 
            +
            # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 16 | 
            +
            # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 17 | 
            +
            # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 18 | 
            +
            # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 19 | 
            +
            # THE SOFTWARE.
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            RSpec.shared_examples Async::Condition do
         | 
| 22 | 
            +
            	it 'can signal waiting task' do
         | 
| 23 | 
            +
            		state = nil
         | 
| 24 | 
            +
            		
         | 
| 25 | 
            +
            		task = reactor.async do
         | 
| 26 | 
            +
            			state = :waiting
         | 
| 27 | 
            +
            			subject.wait
         | 
| 28 | 
            +
            			state = :resumed
         | 
| 29 | 
            +
            		end
         | 
| 30 | 
            +
            		
         | 
| 31 | 
            +
            		expect(state).to be == :waiting
         | 
| 32 | 
            +
            		
         | 
| 33 | 
            +
            		subject.signal
         | 
| 34 | 
            +
            		
         | 
| 35 | 
            +
            		reactor.yield
         | 
| 36 | 
            +
            		
         | 
| 37 | 
            +
            		expect(state).to be == :resumed
         | 
| 38 | 
            +
            	end
         | 
| 39 | 
            +
            	
         | 
| 40 | 
            +
            	it 'should be able to signal stopped task' do
         | 
| 41 | 
            +
            		expect(subject.empty?).to be_truthy
         | 
| 42 | 
            +
            		
         | 
| 43 | 
            +
            		task = reactor.async do
         | 
| 44 | 
            +
            			subject.wait
         | 
| 45 | 
            +
            		end
         | 
| 46 | 
            +
            		
         | 
| 47 | 
            +
            		expect(subject.empty?).to be_falsey
         | 
| 48 | 
            +
            		
         | 
| 49 | 
            +
            		task.stop
         | 
| 50 | 
            +
            		
         | 
| 51 | 
            +
            		subject.signal
         | 
| 52 | 
            +
            	end
         | 
| 53 | 
            +
            end
         | 
| @@ -20,6 +20,8 @@ | |
| 20 20 |  | 
| 21 21 | 
             
            require 'async/condition'
         | 
| 22 22 |  | 
| 23 | 
            +
            require_relative 'condition_examples'
         | 
| 24 | 
            +
             | 
| 23 25 | 
             
            RSpec.describe Async::Condition do
         | 
| 24 26 | 
             
            	include_context Async::RSpec::Reactor
         | 
| 25 27 |  | 
| @@ -38,4 +40,6 @@ RSpec.describe Async::Condition do | |
| 38 40 |  | 
| 39 41 | 
             
            		task.stop
         | 
| 40 42 | 
             
            	end
         | 
| 43 | 
            +
            	
         | 
| 44 | 
            +
            	it_behaves_like Async::Condition
         | 
| 41 45 | 
             
            end
         | 
| @@ -0,0 +1,63 @@ | |
| 1 | 
            +
            # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
         | 
| 2 | 
            +
            # 
         | 
| 3 | 
            +
            # Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 4 | 
            +
            # of this software and associated documentation files (the "Software"), to deal
         | 
| 5 | 
            +
            # in the Software without restriction, including without limitation the rights
         | 
| 6 | 
            +
            # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 7 | 
            +
            # copies of the Software, and to permit persons to whom the Software is
         | 
| 8 | 
            +
            # furnished to do so, subject to the following conditions:
         | 
| 9 | 
            +
            # 
         | 
| 10 | 
            +
            # The above copyright notice and this permission notice shall be included in
         | 
| 11 | 
            +
            # all copies or substantial portions of the Software.
         | 
| 12 | 
            +
            # 
         | 
| 13 | 
            +
            # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 14 | 
            +
            # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 15 | 
            +
            # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 16 | 
            +
            # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 17 | 
            +
            # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 18 | 
            +
            # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 19 | 
            +
            # THE SOFTWARE.
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            require 'async/notification'
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            require_relative 'condition_examples'
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            RSpec.describe Async::Notification do
         | 
| 26 | 
            +
            	include_context Async::RSpec::Reactor
         | 
| 27 | 
            +
            	
         | 
| 28 | 
            +
            	it 'should continue after notification is signalled' do
         | 
| 29 | 
            +
            		sequence = []
         | 
| 30 | 
            +
            		
         | 
| 31 | 
            +
            		task = reactor.async do
         | 
| 32 | 
            +
            			sequence << :waiting
         | 
| 33 | 
            +
            			subject.wait
         | 
| 34 | 
            +
            			sequence << :resumed
         | 
| 35 | 
            +
            		end
         | 
| 36 | 
            +
            		
         | 
| 37 | 
            +
            		expect(task.status).to be :running
         | 
| 38 | 
            +
            		
         | 
| 39 | 
            +
            		sequence << :running
         | 
| 40 | 
            +
            		# This will cause the task to exit:
         | 
| 41 | 
            +
            		subject.signal
         | 
| 42 | 
            +
            		sequence << :signalled
         | 
| 43 | 
            +
            		
         | 
| 44 | 
            +
            		expect(task.status).to be :running
         | 
| 45 | 
            +
            		
         | 
| 46 | 
            +
            		sequence << :yielding
         | 
| 47 | 
            +
            		reactor.yield
         | 
| 48 | 
            +
            		sequence << :finished
         | 
| 49 | 
            +
            		
         | 
| 50 | 
            +
            		expect(task.status).to be :complete
         | 
| 51 | 
            +
            		
         | 
| 52 | 
            +
            		expect(sequence).to be == [
         | 
| 53 | 
            +
            			:waiting,
         | 
| 54 | 
            +
            			:running,
         | 
| 55 | 
            +
            			:signalled,
         | 
| 56 | 
            +
            			:yielding,
         | 
| 57 | 
            +
            			:resumed,
         | 
| 58 | 
            +
            			:finished
         | 
| 59 | 
            +
            		]
         | 
| 60 | 
            +
            	end
         | 
| 61 | 
            +
            	
         | 
| 62 | 
            +
            	it_behaves_like Async::Condition
         | 
| 63 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: async
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 1. | 
| 4 | 
            +
              version: 1.4.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Samuel Williams
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2018-03- | 
| 11 | 
            +
            date: 2018-03-21 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: nio4r
         | 
| @@ -115,12 +115,14 @@ files: | |
| 115 115 | 
             
            - async.gemspec
         | 
| 116 116 | 
             
            - benchmark/async_vs_lightio.rb
         | 
| 117 117 | 
             
            - examples/async_method.rb
         | 
| 118 | 
            +
            - examples/fibers.rb
         | 
| 118 119 | 
             
            - examples/sleep_sort.rb
         | 
| 119 120 | 
             
            - lib/async.rb
         | 
| 120 121 | 
             
            - lib/async/condition.rb
         | 
| 121 122 | 
             
            - lib/async/logger.rb
         | 
| 122 123 | 
             
            - lib/async/measure.rb
         | 
| 123 124 | 
             
            - lib/async/node.rb
         | 
| 125 | 
            +
            - lib/async/notification.rb
         | 
| 124 126 | 
             
            - lib/async/reactor.rb
         | 
| 125 127 | 
             
            - lib/async/task.rb
         | 
| 126 128 | 
             
            - lib/async/version.rb
         | 
| @@ -129,8 +131,10 @@ files: | |
| 129 131 | 
             
            - logo.svg
         | 
| 130 132 | 
             
            - papers/1982 Grossman.pdf
         | 
| 131 133 | 
             
            - papers/1987 ODell.pdf
         | 
| 134 | 
            +
            - spec/async/condition_examples.rb
         | 
| 132 135 | 
             
            - spec/async/condition_spec.rb
         | 
| 133 136 | 
             
            - spec/async/node_spec.rb
         | 
| 137 | 
            +
            - spec/async/notification_spec.rb
         | 
| 134 138 | 
             
            - spec/async/performance_spec.rb
         | 
| 135 139 | 
             
            - spec/async/reactor/nested_spec.rb
         | 
| 136 140 | 
             
            - spec/async/reactor_spec.rb
         | 
| @@ -162,8 +166,10 @@ signing_key: | |
| 162 166 | 
             
            specification_version: 4
         | 
| 163 167 | 
             
            summary: Async is an asynchronous I/O framework based on nio4r.
         | 
| 164 168 | 
             
            test_files:
         | 
| 169 | 
            +
            - spec/async/condition_examples.rb
         | 
| 165 170 | 
             
            - spec/async/condition_spec.rb
         | 
| 166 171 | 
             
            - spec/async/node_spec.rb
         | 
| 172 | 
            +
            - spec/async/notification_spec.rb
         | 
| 167 173 | 
             
            - spec/async/performance_spec.rb
         | 
| 168 174 | 
             
            - spec/async/reactor/nested_spec.rb
         | 
| 169 175 | 
             
            - spec/async/reactor_spec.rb
         |