From 17f722ca47d16b594fad2fb03275d638dfa5548d Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Thu, 21 May 2015 15:35:41 -0600 Subject: [PATCH 01/11] New namespace for SynchronizationObject. --- lib/concurrent.rb | 2 +- lib/concurrent/actor.rb | 2 +- lib/concurrent/actor/core.rb | 2 +- lib/concurrent/at_exit.rb | 4 +- lib/concurrent/atomic/atomic_boolean.rb | 4 +- lib/concurrent/atomic/atomic_fixnum.rb | 4 +- lib/concurrent/atomic/condition.rb | 2 +- .../atomic/copy_on_notify_observer_set.rb | 4 +- .../atomic/copy_on_write_observer_set.rb | 4 +- lib/concurrent/atomic/count_down_latch.rb | 4 +- lib/concurrent/atomic/cyclic_barrier.rb | 4 +- lib/concurrent/atomic/event.rb | 4 +- lib/concurrent/atomic/semaphore.rb | 4 +- .../channel/blocking_ring_buffer.rb | 4 +- lib/concurrent/channel/waitable_list.rb | 4 +- lib/concurrent/delay.rb | 4 +- lib/concurrent/edge/future.rb | 4 +- lib/concurrent/executor/executor_service.rb | 4 +- lib/concurrent/executor/safe_task_executor.rb | 4 +- .../executor/serialized_execution.rb | 4 +- lib/concurrent/ivar.rb | 4 +- lib/concurrent/native_extensions/before.rb | 2 +- lib/concurrent/struct/immutable_struct.rb | 4 +- lib/concurrent/struct/mutable_struct.rb | 6 +- lib/concurrent/struct/settable_struct.rb | 6 +- lib/concurrent/synchronization.rb | 14 --- .../object.rb => synchronization_object.rb} | 18 ++- .../abstract_object.rb | 2 +- .../java_pure_object.rb | 2 +- .../monitor_object.rb | 2 +- .../mutex_object.rb | 2 +- .../rbx_object.rb | 2 +- lib/concurrent/utility/monotonic_time.rb | 4 +- .../concurrent/synchronization_object_spec.rb | 108 +++++++++++++++++ spec/concurrent/synchronization_spec.rb | 110 ------------------ 35 files changed, 175 insertions(+), 183 deletions(-) delete mode 100644 lib/concurrent/synchronization.rb rename lib/concurrent/{synchronization/object.rb => synchronization_object.rb} (51%) rename lib/concurrent/{synchronization => synchronization_object_impl}/abstract_object.rb (99%) rename lib/concurrent/{synchronization => synchronization_object_impl}/java_pure_object.rb (96%) rename lib/concurrent/{synchronization => synchronization_object_impl}/monitor_object.rb (92%) rename lib/concurrent/{synchronization => synchronization_object_impl}/mutex_object.rb (96%) rename lib/concurrent/{synchronization => synchronization_object_impl}/rbx_object.rb (98%) create mode 100644 spec/concurrent/synchronization_object_spec.rb delete mode 100644 spec/concurrent/synchronization_spec.rb diff --git a/lib/concurrent.rb b/lib/concurrent.rb index 54dce984c..99ac87e1c 100644 --- a/lib/concurrent.rb +++ b/lib/concurrent.rb @@ -1,6 +1,6 @@ require 'concurrent/version' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' require 'concurrent/at_exit' require 'concurrent/configuration' diff --git a/lib/concurrent/actor.rb b/lib/concurrent/actor.rb index 0b7055a12..e5af993fa 100644 --- a/lib/concurrent/actor.rb +++ b/lib/concurrent/actor.rb @@ -1,7 +1,7 @@ require 'concurrent/configuration' require 'concurrent/executor/serialized_execution' require 'concurrent/logging' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' require 'concurrent/edge/future' module Concurrent diff --git a/lib/concurrent/actor/core.rb b/lib/concurrent/actor/core.rb index 4cfb84826..0bf72ad77 100644 --- a/lib/concurrent/actor/core.rb +++ b/lib/concurrent/actor/core.rb @@ -11,7 +11,7 @@ module Actor # @note devel: core should not block on anything, e.g. it cannot wait on # children to terminate that would eat up all threads in task pool and # deadlock - class Core < Synchronization::Object + class Core < SynchronizationObject include TypeCheck include Concurrent::Logging diff --git a/lib/concurrent/at_exit.rb b/lib/concurrent/at_exit.rb index eb530e5fd..fe6b1491a 100644 --- a/lib/concurrent/at_exit.rb +++ b/lib/concurrent/at_exit.rb @@ -1,11 +1,11 @@ require 'concurrent/logging' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent # Provides ability to add and remove handlers to be run at `Kernel#at_exit`, order is undefined. # Each handler is executed at most once. - class AtExitImplementation < Synchronization::Object + class AtExitImplementation < SynchronizationObject include Logging # Add a handler to be run at `Kernel#at_exit` diff --git a/lib/concurrent/atomic/atomic_boolean.rb b/lib/concurrent/atomic/atomic_boolean.rb index 1df03a989..76d7c96f2 100644 --- a/lib/concurrent/atomic/atomic_boolean.rb +++ b/lib/concurrent/atomic/atomic_boolean.rb @@ -1,5 +1,5 @@ require 'concurrent/native_extensions' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -22,7 +22,7 @@ module Concurrent # 3.340000 0.010000 3.350000 ( 0.855000) # # @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicBoolean.html java.util.concurrent.atomic.AtomicBoolean - class MutexAtomicBoolean < Synchronization::Object + class MutexAtomicBoolean < SynchronizationObject # @!macro [attach] atomic_boolean_method_initialize # diff --git a/lib/concurrent/atomic/atomic_fixnum.rb b/lib/concurrent/atomic/atomic_fixnum.rb index 37b806b62..333dc0148 100644 --- a/lib/concurrent/atomic/atomic_fixnum.rb +++ b/lib/concurrent/atomic/atomic_fixnum.rb @@ -1,5 +1,5 @@ require 'concurrent/native_extensions' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -22,7 +22,7 @@ module Concurrent # 4.520000 0.030000 4.550000 ( 1.187000) # # @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicLong.html java.util.concurrent.atomic.AtomicLong - class MutexAtomicFixnum < Synchronization::Object + class MutexAtomicFixnum < SynchronizationObject # http://stackoverflow.com/questions/535721/ruby-max-integer MIN_VALUE = -(2**(0.size * 8 - 2)) diff --git a/lib/concurrent/atomic/condition.rb b/lib/concurrent/atomic/condition.rb index 72598cad5..aa1d146b6 100644 --- a/lib/concurrent/atomic/condition.rb +++ b/lib/concurrent/atomic/condition.rb @@ -39,7 +39,7 @@ def timed_out? end def initialize - warn '[DEPRECATED] Will be replaced with Synchronization::Object in v1.0.' + warn '[DEPRECATED] Will be replaced with SynchronizationObject in v1.0.' @condition = ConditionVariable.new end diff --git a/lib/concurrent/atomic/copy_on_notify_observer_set.rb b/lib/concurrent/atomic/copy_on_notify_observer_set.rb index c1a133e0e..951aa445d 100644 --- a/lib/concurrent/atomic/copy_on_notify_observer_set.rb +++ b/lib/concurrent/atomic/copy_on_notify_observer_set.rb @@ -1,4 +1,4 @@ -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -6,7 +6,7 @@ module Concurrent # observers are added and removed from a thread safe collection; every time # a notification is required the internal data structure is copied to # prevent concurrency issues - class CopyOnNotifyObserverSet < Synchronization::Object + class CopyOnNotifyObserverSet < SynchronizationObject # Adds an observer to this set. If a block is passed, the observer will be # created by this method and no other params should be passed diff --git a/lib/concurrent/atomic/copy_on_write_observer_set.rb b/lib/concurrent/atomic/copy_on_write_observer_set.rb index 2ed2ffcbc..de70c7a45 100644 --- a/lib/concurrent/atomic/copy_on_write_observer_set.rb +++ b/lib/concurrent/atomic/copy_on_write_observer_set.rb @@ -1,11 +1,11 @@ -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent # A thread safe observer set implemented using copy-on-write approach: # every time an observer is added or removed the whole internal data structure is # duplicated and replaced with a new one. - class CopyOnWriteObserverSet < Synchronization::Object + class CopyOnWriteObserverSet < SynchronizationObject # Adds an observer to this set # If a block is passed, the observer will be created by this method and no diff --git a/lib/concurrent/atomic/count_down_latch.rb b/lib/concurrent/atomic/count_down_latch.rb index 183a98039..c885d59c5 100644 --- a/lib/concurrent/atomic/count_down_latch.rb +++ b/lib/concurrent/atomic/count_down_latch.rb @@ -1,4 +1,4 @@ -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -11,7 +11,7 @@ module Concurrent # method. Each of the other threads calls `#count_down` when done with its work. # When the latch counter reaches zero the waiting thread is unblocked and continues # with its work. A `CountDownLatch` can be used only once. Its value cannot be reset. - class PureCountDownLatch < Synchronization::Object + class PureCountDownLatch < SynchronizationObject # @!macro [attach] count_down_latch_method_initialize # diff --git a/lib/concurrent/atomic/cyclic_barrier.rb b/lib/concurrent/atomic/cyclic_barrier.rb index 03dac3ed4..c544d5d16 100644 --- a/lib/concurrent/atomic/cyclic_barrier.rb +++ b/lib/concurrent/atomic/cyclic_barrier.rb @@ -1,8 +1,8 @@ -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent - class CyclicBarrier < Synchronization::Object + class CyclicBarrier < SynchronizationObject Generation = Struct.new(:status) private_constant :Generation diff --git a/lib/concurrent/atomic/event.rb b/lib/concurrent/atomic/event.rb index b0f1f80e7..447e35d2c 100644 --- a/lib/concurrent/atomic/event.rb +++ b/lib/concurrent/atomic/event.rb @@ -1,5 +1,5 @@ require 'thread' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -13,7 +13,7 @@ module Concurrent # `#reset` at any time once it has been set. # # @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms682655.aspx - class Event < Synchronization::Object + class Event < SynchronizationObject # Creates a new `Event` in the unset state. Threads calling `#wait` on the # `Event` will block. diff --git a/lib/concurrent/atomic/semaphore.rb b/lib/concurrent/atomic/semaphore.rb index 4a7ac1cc8..f3419021e 100644 --- a/lib/concurrent/atomic/semaphore.rb +++ b/lib/concurrent/atomic/semaphore.rb @@ -1,4 +1,4 @@ -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -10,7 +10,7 @@ module Concurrent # releasing a blocking acquirer. # However, no actual permit objects are used; the Semaphore just keeps a # count of the number available and acts accordingly. - class MutexSemaphore < Synchronization::Object + class MutexSemaphore < SynchronizationObject # @!macro [attach] semaphore_method_initialize # diff --git a/lib/concurrent/channel/blocking_ring_buffer.rb b/lib/concurrent/channel/blocking_ring_buffer.rb index 41d7b7cd8..4d8650e5d 100644 --- a/lib/concurrent/channel/blocking_ring_buffer.rb +++ b/lib/concurrent/channel/blocking_ring_buffer.rb @@ -1,7 +1,7 @@ -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent - class BlockingRingBuffer < Synchronization::Object + class BlockingRingBuffer < SynchronizationObject def initialize(capacity) super(capacity) diff --git a/lib/concurrent/channel/waitable_list.rb b/lib/concurrent/channel/waitable_list.rb index d7cebedd9..415fbbb8b 100644 --- a/lib/concurrent/channel/waitable_list.rb +++ b/lib/concurrent/channel/waitable_list.rb @@ -1,7 +1,7 @@ -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent - class WaitableList < Synchronization::Object + class WaitableList < SynchronizationObject def size synchronize { @list.size } diff --git a/lib/concurrent/delay.rb b/lib/concurrent/delay.rb index 2e661c2ee..e9c8d31ce 100644 --- a/lib/concurrent/delay.rb +++ b/lib/concurrent/delay.rb @@ -3,7 +3,7 @@ require 'concurrent/obligation' require 'concurrent/executor/executor' require 'concurrent/executor/immediate_executor' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -38,7 +38,7 @@ module Concurrent # execute on the given executor, allowing the call to timeout. # # @see Concurrent::Dereferenceable - class Delay < Synchronization::Object + class Delay < SynchronizationObject include Obligation # NOTE: Because the global thread pools are lazy-loaded with these objects diff --git a/lib/concurrent/edge/future.rb b/lib/concurrent/edge/future.rb index f999404bf..9929be729 100644 --- a/lib/concurrent/edge/future.rb +++ b/lib/concurrent/edge/future.rb @@ -81,7 +81,7 @@ def post_on(executor, *args, &job) extend FutureShortcuts include FutureShortcuts - class Event < Synchronization::Object + class Event < SynchronizationObject extend FutureShortcuts attr_volatile :state @@ -546,7 +546,7 @@ def evaluate_to!(*args, &block) # TODO modularize blocked_by and notify blocked # @abstract - class AbstractPromise < Synchronization::Object + class AbstractPromise < SynchronizationObject def initialize(future, *args, &block) super(*args, &block) @Future = future diff --git a/lib/concurrent/executor/executor_service.rb b/lib/concurrent/executor/executor_service.rb index 683a60aa1..ddc248797 100644 --- a/lib/concurrent/executor/executor_service.rb +++ b/lib/concurrent/executor/executor_service.rb @@ -2,7 +2,7 @@ require 'concurrent/logging' require 'concurrent/at_exit' require 'concurrent/atomic/event' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -90,7 +90,7 @@ def serialized? end end - class AbstractExecutorService < Synchronization::Object + class AbstractExecutorService < SynchronizationObject include ExecutorService # The set of possible fallback policies that may be set at thread pool creation. diff --git a/lib/concurrent/executor/safe_task_executor.rb b/lib/concurrent/executor/safe_task_executor.rb index 9ef5b2eef..b3c849bf9 100644 --- a/lib/concurrent/executor/safe_task_executor.rb +++ b/lib/concurrent/executor/safe_task_executor.rb @@ -1,4 +1,4 @@ -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -6,7 +6,7 @@ module Concurrent # success - indicating if the callable has been executed without errors # value - filled by the callable result if it has been executed without errors, nil otherwise # reason - the error risen by the callable if it has been executed with errors, nil otherwise - class SafeTaskExecutor < Synchronization::Object + class SafeTaskExecutor < SynchronizationObject def initialize(task, opts = {}) super() diff --git a/lib/concurrent/executor/serialized_execution.rb b/lib/concurrent/executor/serialized_execution.rb index 1ec6ec90f..0d8b6ad1d 100644 --- a/lib/concurrent/executor/serialized_execution.rb +++ b/lib/concurrent/executor/serialized_execution.rb @@ -1,12 +1,12 @@ require 'delegate' require 'concurrent/executor/executor_service' require 'concurrent/logging' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent # Ensures passed jobs in a serialized order never running at the same time. - class SerializedExecution < Synchronization::Object + class SerializedExecution < SynchronizationObject include Logging Job = Struct.new(:executor, :args, :block) do diff --git a/lib/concurrent/ivar.rb b/lib/concurrent/ivar.rb index 631e63d20..43c41ab68 100644 --- a/lib/concurrent/ivar.rb +++ b/lib/concurrent/ivar.rb @@ -3,7 +3,7 @@ require 'concurrent/errors' require 'concurrent/obligation' require 'concurrent/observable' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -39,7 +39,7 @@ module Concurrent # ivar.set 14 # ivar.get #=> 14 # ivar.set 2 # would now be an error - class IVar < Synchronization::Object + class IVar < SynchronizationObject include Obligation include Observable diff --git a/lib/concurrent/native_extensions/before.rb b/lib/concurrent/native_extensions/before.rb index 361abe434..79937938a 100644 --- a/lib/concurrent/native_extensions/before.rb +++ b/lib/concurrent/native_extensions/before.rb @@ -1 +1 @@ -require 'concurrent/synchronization/abstract_object' +require 'concurrent/synchronization_object_impl/abstract_object' diff --git a/lib/concurrent/struct/immutable_struct.rb b/lib/concurrent/struct/immutable_struct.rb index 4b0157765..a20c4900e 100644 --- a/lib/concurrent/struct/immutable_struct.rb +++ b/lib/concurrent/struct/immutable_struct.rb @@ -1,5 +1,5 @@ require 'concurrent/struct/abstract_struct' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -80,7 +80,7 @@ def self.new(*args, &block) FACTORY.define_struct(clazz_name, args, &block) end - FACTORY = Class.new(Synchronization::Object) do + FACTORY = Class.new(SynchronizationObject) do def define_struct(name, members, &block) synchronize do AbstractStruct.define_struct_class(ImmutableStruct, nil, name, members, &block) diff --git a/lib/concurrent/struct/mutable_struct.rb b/lib/concurrent/struct/mutable_struct.rb index 9c3efa6bb..4509fa902 100644 --- a/lib/concurrent/struct/mutable_struct.rb +++ b/lib/concurrent/struct/mutable_struct.rb @@ -1,5 +1,5 @@ require 'concurrent/struct/abstract_struct' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -206,10 +206,10 @@ def self.new(*args, &block) FACTORY.define_struct(clazz_name, args, &block) end - FACTORY = Class.new(Synchronization::Object) do + FACTORY = Class.new(SynchronizationObject) do def define_struct(name, members, &block) synchronize do - clazz = AbstractStruct.define_struct_class(MutableStruct, Synchronization::Object, name, members, &block) + clazz = AbstractStruct.define_struct_class(MutableStruct, SynchronizationObject, name, members, &block) members.each_with_index do |member, index| clazz.send(:define_method, member) do synchronize { @values[index] } diff --git a/lib/concurrent/struct/settable_struct.rb b/lib/concurrent/struct/settable_struct.rb index 09fe705b8..c6e7f0398 100644 --- a/lib/concurrent/struct/settable_struct.rb +++ b/lib/concurrent/struct/settable_struct.rb @@ -1,6 +1,6 @@ require 'concurrent/struct/abstract_struct' require 'concurrent/errors' -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent @@ -101,10 +101,10 @@ def self.new(*args, &block) FACTORY.define_struct(clazz_name, args, &block) end - FACTORY = Class.new(Synchronization::Object) do + FACTORY = Class.new(SynchronizationObject) do def define_struct(name, members, &block) synchronize do - clazz = AbstractStruct.define_struct_class(SettableStruct, Synchronization::Object, name, members, &block) + clazz = AbstractStruct.define_struct_class(SettableStruct, SynchronizationObject, name, members, &block) members.each_with_index do |member, index| clazz.send(:define_method, member) do synchronize { @values[index] } diff --git a/lib/concurrent/synchronization.rb b/lib/concurrent/synchronization.rb deleted file mode 100644 index 9c63c6259..000000000 --- a/lib/concurrent/synchronization.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'concurrent/utility/engine' -require 'concurrent/synchronization/abstract_object' -require 'concurrent/native_extensions' # JavaObject -require 'concurrent/synchronization/mutex_object' -require 'concurrent/synchronization/monitor_object' -require 'concurrent/synchronization/rbx_object' -require 'concurrent/synchronization/object' - -module Concurrent - # {include:file:doc/synchronization.md} - module Synchronization - end -end - diff --git a/lib/concurrent/synchronization/object.rb b/lib/concurrent/synchronization_object.rb similarity index 51% rename from lib/concurrent/synchronization/object.rb rename to lib/concurrent/synchronization_object.rb index 44e808a5c..1fdaa3086 100644 --- a/lib/concurrent/synchronization/object.rb +++ b/lib/concurrent/synchronization_object.rb @@ -1,5 +1,14 @@ +require 'concurrent/utility/engine' +require 'concurrent/native_extensions' # JavaObject +require 'concurrent/synchronization_object_impl/abstract_object' +require 'concurrent/synchronization_object_impl/mutex_object' +require 'concurrent/synchronization_object_impl/monitor_object' +require 'concurrent/synchronization_object_impl/rbx_object' + module Concurrent - module Synchronization + + # {include:file:doc/synchronization.md} + module SynchronizationObjectImpl Implementation = case when Concurrent.on_jruby? JavaObject @@ -13,10 +22,9 @@ module Synchronization warn 'Possibly unsupported Ruby implementation' MutexObject end - private_constant :Implementation + end - # @see AbstractObject AbstractObject which defines interface of this class. - class Object < Implementation - end + # @see AbstractObject AbstractObject which defines interface of this class. + class SynchronizationObject < SynchronizationObjectImpl::Implementation end end diff --git a/lib/concurrent/synchronization/abstract_object.rb b/lib/concurrent/synchronization_object_impl/abstract_object.rb similarity index 99% rename from lib/concurrent/synchronization/abstract_object.rb rename to lib/concurrent/synchronization_object_impl/abstract_object.rb index e56cf84e3..9901f3f17 100644 --- a/lib/concurrent/synchronization/abstract_object.rb +++ b/lib/concurrent/synchronization_object_impl/abstract_object.rb @@ -1,5 +1,5 @@ module Concurrent - module Synchronization + module SynchronizationObjectImpl # Safe synchronization under any Ruby implementation. # It provides methods like {#synchronize}, {#ns_wait}, {#ns_signal} and {#ns_broadcast}. # Provides a single layer which can improve its implementation over time without changes needed to diff --git a/lib/concurrent/synchronization/java_pure_object.rb b/lib/concurrent/synchronization_object_impl/java_pure_object.rb similarity index 96% rename from lib/concurrent/synchronization/java_pure_object.rb rename to lib/concurrent/synchronization_object_impl/java_pure_object.rb index ff4ec210c..de9d2de4f 100644 --- a/lib/concurrent/synchronization/java_pure_object.rb +++ b/lib/concurrent/synchronization_object_impl/java_pure_object.rb @@ -1,5 +1,5 @@ module Concurrent - module Synchronization + module SynchronizationObjectImpl if Concurrent.on_jruby? require 'jruby' diff --git a/lib/concurrent/synchronization/monitor_object.rb b/lib/concurrent/synchronization_object_impl/monitor_object.rb similarity index 92% rename from lib/concurrent/synchronization/monitor_object.rb rename to lib/concurrent/synchronization_object_impl/monitor_object.rb index 661e93afd..8a48da501 100644 --- a/lib/concurrent/synchronization/monitor_object.rb +++ b/lib/concurrent/synchronization_object_impl/monitor_object.rb @@ -1,5 +1,5 @@ module Concurrent - module Synchronization + module SynchronizationObjectImpl class MonitorObject < MutexObject def initialize(*args, &block) @__lock__ = ::Monitor.new diff --git a/lib/concurrent/synchronization/mutex_object.rb b/lib/concurrent/synchronization_object_impl/mutex_object.rb similarity index 96% rename from lib/concurrent/synchronization/mutex_object.rb rename to lib/concurrent/synchronization_object_impl/mutex_object.rb index 1a8a6d95c..46b80fce4 100644 --- a/lib/concurrent/synchronization/mutex_object.rb +++ b/lib/concurrent/synchronization_object_impl/mutex_object.rb @@ -1,5 +1,5 @@ module Concurrent - module Synchronization + module SynchronizationObjectImpl class MutexObject < AbstractObject def initialize(*args, &block) @__lock__ = ::Mutex.new diff --git a/lib/concurrent/synchronization/rbx_object.rb b/lib/concurrent/synchronization_object_impl/rbx_object.rb similarity index 98% rename from lib/concurrent/synchronization/rbx_object.rb rename to lib/concurrent/synchronization_object_impl/rbx_object.rb index fd42fad28..184871b83 100644 --- a/lib/concurrent/synchronization/rbx_object.rb +++ b/lib/concurrent/synchronization_object_impl/rbx_object.rb @@ -1,5 +1,5 @@ module Concurrent - module Synchronization + module SynchronizationObjectImpl if Concurrent.on_rbx? class RbxObject < AbstractObject def initialize(*args, &block) diff --git a/lib/concurrent/utility/monotonic_time.rb b/lib/concurrent/utility/monotonic_time.rb index 2b2bcd263..235114e1e 100644 --- a/lib/concurrent/utility/monotonic_time.rb +++ b/lib/concurrent/utility/monotonic_time.rb @@ -1,11 +1,11 @@ -require 'concurrent/synchronization' +require 'concurrent/synchronization_object' module Concurrent # Clock that cannot be set and represents monotonic time since # some unspecified starting point. # @!visibility private - GLOBAL_MONOTONIC_CLOCK = Class.new(Synchronization::Object) { + GLOBAL_MONOTONIC_CLOCK = Class.new(SynchronizationObject) { if defined?(Process::CLOCK_MONOTONIC) # @!visibility private diff --git a/spec/concurrent/synchronization_object_spec.rb b/spec/concurrent/synchronization_object_spec.rb new file mode 100644 index 000000000..3adf67a3d --- /dev/null +++ b/spec/concurrent/synchronization_object_spec.rb @@ -0,0 +1,108 @@ +module Concurrent + + describe SynchronizationObject do + + class AClass < SynchronizationObject + attr_volatile :volatile + attr_accessor :not_volatile + + def initialize(value = nil) + super() + @Final = value + ensure_ivar_visibility! + end + + def final + @Final + end + + def count + synchronize { @count += 1 } + end + + def wait(timeout = nil) + synchronize { ns_wait(timeout) } + end + + private + + def ns_initialize + @count = 0 + end + end + + subject { AClass.new } + + describe '#wait' do + + it 'waiting thread is sleeping' do + t = Thread.new do + Thread.abort_on_exception = true + subject.wait + end + sleep 0.1 + expect(t.status).to eq 'sleep' + end + + it 'sleeping thread can be killed' do + t = Thread.new do + Thread.abort_on_exception = true + subject.wait rescue nil + end + sleep 0.1 + t.kill + sleep 0.1 + expect(t.status).to eq false + expect(t.alive?).to eq false + end + end + + describe '#synchronize' do + it 'allows only one thread to execute count' do + threads = 10.times.map { Thread.new(subject) { 100.times { subject.count } } } + threads.each(&:join) + expect(subject.count).to eq 1001 + end + end + + describe 'signaling' do + pending 'for now pending, tested pretty well by Event' + end + + specify 'final field always visible' do + store = AClass.new 'asd' + t1 = Thread.new { 1000000000.times { |i| store = AClass.new i.to_s } } + t2 = Thread.new { 10.times { expect(store.final).not_to be_nil; Thread.pass } } + t2.join + t1.kill + end + + describe 'attr volatile' do + specify 'older writes are always visible' do + store = AClass.new + store.not_volatile = 0 + store.volatile = 0 + + t1 = Thread.new do + Thread.abort_on_exception = true + 1000000000.times do |i| + store.not_volatile = i + store.volatile = i + end + end + + t2 = Thread.new do + 10.times do + volatile = store.volatile + not_volatile = store.not_volatile + expect(not_volatile).to be >= volatile + Thread.pass + end + end + + t2.join + t1.kill + end + end + end +end diff --git a/spec/concurrent/synchronization_spec.rb b/spec/concurrent/synchronization_spec.rb deleted file mode 100644 index 87f4e199c..000000000 --- a/spec/concurrent/synchronization_spec.rb +++ /dev/null @@ -1,110 +0,0 @@ -module Concurrent - - describe Synchronization do - describe Synchronization::Object do - - class AClass < Synchronization::Object - attr_volatile :volatile - attr_accessor :not_volatile - - def initialize(value = nil) - super() - @Final = value - ensure_ivar_visibility! - end - - def final - @Final - end - - def count - synchronize { @count += 1 } - end - - def wait(timeout = nil) - synchronize { ns_wait(timeout) } - end - - private - - def ns_initialize - @count = 0 - end - end - - subject { AClass.new } - - describe '#wait' do - - it 'waiting thread is sleeping' do - t = Thread.new do - Thread.abort_on_exception = true - subject.wait - end - sleep 0.1 - expect(t.status).to eq 'sleep' - end - - it 'sleeping thread can be killed' do - t = Thread.new do - Thread.abort_on_exception = true - subject.wait rescue nil - end - sleep 0.1 - t.kill - sleep 0.1 - expect(t.status).to eq false - expect(t.alive?).to eq false - end - end - - describe '#synchronize' do - it 'allows only one thread to execute count' do - threads = 10.times.map { Thread.new(subject) { 100.times { subject.count } } } - threads.each(&:join) - expect(subject.count).to eq 1001 - end - end - - describe 'signaling' do - pending 'for now pending, tested pretty well by Event' - end - - specify 'final field always visible' do - store = AClass.new 'asd' - t1 = Thread.new { 1000000000.times { |i| store = AClass.new i.to_s } } - t2 = Thread.new { 10.times { expect(store.final).not_to be_nil; Thread.pass } } - t2.join - t1.kill - end - - describe 'attr volatile' do - specify 'older writes are always visible' do - store = AClass.new - store.not_volatile = 0 - store.volatile = 0 - - t1 = Thread.new do - Thread.abort_on_exception = true - 1000000000.times do |i| - store.not_volatile = i - store.volatile = i - end - end - - t2 = Thread.new do - 10.times do - volatile = store.volatile - not_volatile = store.not_volatile - expect(not_volatile).to be >= volatile - Thread.pass - end - end - - t2.join - t1.kill - end - end - end - end -end From 88ace51b1372e72ea52e613d3b6f9b3aa892cdf6 Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Thu, 21 May 2015 16:18:08 -0600 Subject: [PATCH 02/11] Moved mixins to Concurrent::Concern --- lib/concurrent/actor.rb | 2 +- lib/concurrent/actor/behaviour/abstract.rb | 5 +- .../actor/behaviour/executes_context.rb | 2 +- .../actor/behaviour/sets_results.rb | 2 +- lib/concurrent/actor/context.rb | 3 + lib/concurrent/actor/core.rb | 2 +- .../actor/default_dead_letter_handler.rb | 2 +- lib/concurrent/agent.rb | 12 +- lib/concurrent/at_exit.rb | 4 +- lib/concurrent/concern/dereferenceable.rb | 108 ++++++ lib/concurrent/concern/logging.rb | 23 ++ lib/concurrent/concern/obligation.rb | 226 ++++++++++++ lib/concurrent/concern/observable.rb | 85 +++++ lib/concurrent/configuration.rb | 3 +- lib/concurrent/delay.rb | 4 +- lib/concurrent/dereferenceable.rb | 106 ------ lib/concurrent/executor/executor_service.rb | 4 +- .../executor/ruby_thread_pool_executor.rb | 3 +- .../executor/serialized_execution.rb | 4 +- lib/concurrent/ivar.rb | 8 +- lib/concurrent/logging.rb | 20 -- lib/concurrent/mvar.rb | 4 +- lib/concurrent/obligation.rb | 224 ------------ lib/concurrent/observable.rb | 83 ----- lib/concurrent/promise.rb | 1 - lib/concurrent/timer_task.rb | 8 +- spec/concurrent/agent_spec.rb | 4 +- spec/concurrent/channel/probe_spec.rb | 2 +- .../{ => concern}/dereferenceable_shared.rb | 0 .../{ => concern}/obligation_shared.rb | 0 spec/concurrent/concern/obligation_spec.rb | 330 ++++++++++++++++++ .../{ => concern}/observable_shared.rb | 0 spec/concurrent/concern/observable_spec.rb | 56 +++ spec/concurrent/delay_spec.rb | 4 +- spec/concurrent/ivar_shared.rb | 6 +- spec/concurrent/mvar_spec.rb | 2 +- spec/concurrent/obligation_spec.rb | 328 ----------------- spec/concurrent/observable_spec.rb | 54 --- spec/concurrent/scheduled_task_spec.rb | 6 +- spec/concurrent/timer_task_spec.rb | 4 +- 40 files changed, 882 insertions(+), 862 deletions(-) create mode 100644 lib/concurrent/concern/dereferenceable.rb create mode 100644 lib/concurrent/concern/logging.rb create mode 100644 lib/concurrent/concern/obligation.rb create mode 100644 lib/concurrent/concern/observable.rb delete mode 100644 lib/concurrent/dereferenceable.rb delete mode 100644 lib/concurrent/logging.rb delete mode 100644 lib/concurrent/obligation.rb delete mode 100644 lib/concurrent/observable.rb rename spec/concurrent/{ => concern}/dereferenceable_shared.rb (100%) rename spec/concurrent/{ => concern}/obligation_shared.rb (100%) create mode 100644 spec/concurrent/concern/obligation_spec.rb rename spec/concurrent/{ => concern}/observable_shared.rb (100%) create mode 100644 spec/concurrent/concern/observable_spec.rb delete mode 100644 spec/concurrent/obligation_spec.rb delete mode 100644 spec/concurrent/observable_spec.rb diff --git a/lib/concurrent/actor.rb b/lib/concurrent/actor.rb index e5af993fa..e71a91a58 100644 --- a/lib/concurrent/actor.rb +++ b/lib/concurrent/actor.rb @@ -1,6 +1,6 @@ require 'concurrent/configuration' require 'concurrent/executor/serialized_execution' -require 'concurrent/logging' +require 'concurrent/concern/logging' require 'concurrent/synchronization_object' require 'concurrent/edge/future' diff --git a/lib/concurrent/actor/behaviour/abstract.rb b/lib/concurrent/actor/behaviour/abstract.rb index a697a28d3..4852ffb6e 100644 --- a/lib/concurrent/actor/behaviour/abstract.rb +++ b/lib/concurrent/actor/behaviour/abstract.rb @@ -1,9 +1,12 @@ +require 'concurrent/concern/logging' + module Concurrent module Actor module Behaviour class Abstract include TypeCheck include InternalDelegations + include Concern::Logging attr_reader :core, :subsequent @@ -39,7 +42,7 @@ def broadcast(event) def reject_envelope(envelope) envelope.reject! ActorTerminated.new(reference) dead_letter_routing << envelope unless envelope.future - log Logging::DEBUG, "rejected #{envelope.message} from #{envelope.sender_path}" + log DEBUG, "rejected #{envelope.message} from #{envelope.sender_path}" end end end diff --git a/lib/concurrent/actor/behaviour/executes_context.rb b/lib/concurrent/actor/behaviour/executes_context.rb index 1b13672d2..bc2d2df79 100644 --- a/lib/concurrent/actor/behaviour/executes_context.rb +++ b/lib/concurrent/actor/behaviour/executes_context.rb @@ -9,7 +9,7 @@ def on_envelope(envelope) def on_event(event) context.on_event(event) - core.log Logging::DEBUG, "event: #{event.inspect}" + core.log DEBUG, "event: #{event.inspect}" super event end end diff --git a/lib/concurrent/actor/behaviour/sets_results.rb b/lib/concurrent/actor/behaviour/sets_results.rb index df3594e9e..a7df70e86 100644 --- a/lib/concurrent/actor/behaviour/sets_results.rb +++ b/lib/concurrent/actor/behaviour/sets_results.rb @@ -17,7 +17,7 @@ def on_envelope(envelope) end nil rescue => error - log Logging::ERROR, error + log ERROR, error case error_strategy when :terminate! terminate! diff --git a/lib/concurrent/actor/context.rb b/lib/concurrent/actor/context.rb index 15d7e7faa..c0b9ec946 100644 --- a/lib/concurrent/actor/context.rb +++ b/lib/concurrent/actor/context.rb @@ -1,3 +1,5 @@ +require 'concurrent/concern/logging' + module Concurrent module Actor @@ -15,6 +17,7 @@ module Actor class AbstractContext include TypeCheck include InternalDelegations + include Concern::Logging attr_reader :core diff --git a/lib/concurrent/actor/core.rb b/lib/concurrent/actor/core.rb index 0bf72ad77..9860adda8 100644 --- a/lib/concurrent/actor/core.rb +++ b/lib/concurrent/actor/core.rb @@ -13,7 +13,7 @@ module Actor # deadlock class Core < SynchronizationObject include TypeCheck - include Concurrent::Logging + include Concurrent::Concern::Logging # @!attribute [r] reference # @return [Reference] reference to this actor which can be safely passed around diff --git a/lib/concurrent/actor/default_dead_letter_handler.rb b/lib/concurrent/actor/default_dead_letter_handler.rb index 90ae9c094..889dbd7f8 100644 --- a/lib/concurrent/actor/default_dead_letter_handler.rb +++ b/lib/concurrent/actor/default_dead_letter_handler.rb @@ -2,7 +2,7 @@ module Concurrent module Actor class DefaultDeadLetterHandler < RestartingContext def on_message(dead_letter) - log Logging::INFO, "got dead letter #{dead_letter.inspect}" + log INFO, "got dead letter #{dead_letter.inspect}" end end end diff --git a/lib/concurrent/agent.rb b/lib/concurrent/agent.rb index 482dfdd8d..1a3388240 100644 --- a/lib/concurrent/agent.rb +++ b/lib/concurrent/agent.rb @@ -1,7 +1,7 @@ require 'thread' -require 'concurrent/dereferenceable' -require 'concurrent/observable' -require 'concurrent/logging' +require 'concurrent/concern/dereferenceable' +require 'concurrent/concern/observable' +require 'concurrent/concern/logging' require 'concurrent/executor/executor' module Concurrent @@ -11,9 +11,9 @@ module Concurrent # @!attribute [r] timeout # @return [Fixnum] the maximum number of seconds before an update is cancelled class Agent - include Dereferenceable - include Observable - include Logging + include Concern::Dereferenceable + include Concern::Observable + include Concern::Logging attr_reader :timeout, :io_executor, :fast_executor diff --git a/lib/concurrent/at_exit.rb b/lib/concurrent/at_exit.rb index fe6b1491a..5547d93d4 100644 --- a/lib/concurrent/at_exit.rb +++ b/lib/concurrent/at_exit.rb @@ -1,4 +1,4 @@ -require 'concurrent/logging' +require 'concurrent/concern/logging' require 'concurrent/synchronization_object' module Concurrent @@ -6,7 +6,7 @@ module Concurrent # Provides ability to add and remove handlers to be run at `Kernel#at_exit`, order is undefined. # Each handler is executed at most once. class AtExitImplementation < SynchronizationObject - include Logging + include Concern::Logging # Add a handler to be run at `Kernel#at_exit` # @param [Object] handler_id optionally provide an id, if allready present, handler is replaced diff --git a/lib/concurrent/concern/dereferenceable.rb b/lib/concurrent/concern/dereferenceable.rb new file mode 100644 index 000000000..52c5478c8 --- /dev/null +++ b/lib/concurrent/concern/dereferenceable.rb @@ -0,0 +1,108 @@ +module Concurrent + module Concern + + # Object references in Ruby are mutable. This can lead to serious problems when + # the `#value` of a concurrent object is a mutable reference. Which is always the + # case unless the value is a `Fixnum`, `Symbol`, or similar "primitive" data type. + # Most classes in this library that expose a `#value` getter method do so using the + # `Dereferenceable` mixin module. + # + # Objects with this mixin can be configured with a few options that can help protect + # the program from potentially dangerous operations. + # + # * `:dup_on_deref` when true will call the `#dup` method on the `value` + # object every time the `#value` method is called (default: false) + # * `:freeze_on_deref` when true will call the `#freeze` method on the `value` object + # every time the `#value` method is called (default: false) + # * `:copy_on_deref` when given a `Proc` object the `Proc` will be run every + # time the `#value` method is called. The `Proc` will be given the current + # `value` as its only parameter and the result returned by the block will + # be the return value of the `#value` call. When `nil` this option will be + # ignored (default: nil) + module Dereferenceable + + # Return the value this object represents after applying the options specified + # by the `#set_deref_options` method. + # + # When multiple deref options are set the order of operations is strictly defined. + # The order of deref operations is: + # * `:copy_on_deref` + # * `:dup_on_deref` + # * `:freeze_on_deref` + # + # Because of this ordering there is no need to `#freeze` an object created by a + # provided `:copy_on_deref` block. Simply set `:freeze_on_deref` to `true`. + # Setting both `:dup_on_deref` to `true` and `:freeze_on_deref` to `true` is + # as close to the behavior of a "pure" functional language (like Erlang, Clojure, + # or Haskell) as we are likely to get in Ruby. + # + # This method is thread-safe and synchronized with the internal `#mutex`. + # + # @return [Object] the current value of the object + def value + mutex.synchronize { apply_deref_options(@value) } + end + alias_method :deref, :value + + protected + + # Set the internal value of this object + # + # @param [Object] value the new value + def value=(value) + mutex.synchronize{ @value = value } + end + + # A mutex lock used for synchronizing thread-safe operations. Methods defined + # by `Dereferenceable` are synchronized using the `Mutex` returned from this + # method. Operations performed by the including class that operate on the + # `@value` instance variable should be locked with this `Mutex`. + # + # @return [Mutex] the synchronization object + def mutex + @mutex + end + + # Initializes the internal `Mutex`. + # + # @note This method *must* be called from within the constructor of the including class. + # + # @see #mutex + def init_mutex(mutex = Mutex.new) + @mutex = mutex + end + + # Set the options which define the operations #value performs before + # returning data to the caller (dereferencing). + # + # @note Most classes that include this module will call `#set_deref_options` + # from within the constructor, thus allowing these options to be set at + # object creation. + # + # @param [Hash] opts the options defining dereference behavior. + # @option opts [String] :dup_on_deref (false) call `#dup` before returning the data + # @option opts [String] :freeze_on_deref (false) call `#freeze` before returning the data + # @option opts [String] :copy_on_deref (nil) call the given `Proc` passing + # the internal value and returning the value returned from the proc + def set_deref_options(opts = {}) + mutex.synchronize do + @dup_on_deref = opts[:dup_on_deref] || opts[:dup] + @freeze_on_deref = opts[:freeze_on_deref] || opts[:freeze] + @copy_on_deref = opts[:copy_on_deref] || opts[:copy] + @do_nothing_on_deref = !(@dup_on_deref || @freeze_on_deref || @copy_on_deref) + nil + end + end + + # @!visibility private + def apply_deref_options(value) # :nodoc: + return nil if value.nil? + return value if @do_nothing_on_deref + value = @copy_on_deref.call(value) if @copy_on_deref + value = value.dup if @dup_on_deref + value = value.freeze if @freeze_on_deref + value + end + end + end +end diff --git a/lib/concurrent/concern/logging.rb b/lib/concurrent/concern/logging.rb new file mode 100644 index 000000000..e9c9739d5 --- /dev/null +++ b/lib/concurrent/concern/logging.rb @@ -0,0 +1,23 @@ +require 'logger' + +module Concurrent + module Concern + + # Include where logging is needed + module Logging + include Logger::Severity + + # Logs through {Configuration#logger}, it can be overridden by setting @logger + # @param [Integer] level one of Logger::Severity constants + # @param [String] progname e.g. a path of an Actor + # @param [String, nil] message when nil block is used to generate the message + # @yieldreturn [String] a message + def log(level, progname, message = nil, &block) + (@logger || Concurrent.global_logger).call level, progname, message, &block + rescue => error + $stderr.puts "`Concurrent.configuration.logger` failed to log #{[level, progname, message, block]}\n" + + "#{error.message} (#{error.class})\n#{error.backtrace.join "\n"}" + end + end + end +end diff --git a/lib/concurrent/concern/obligation.rb b/lib/concurrent/concern/obligation.rb new file mode 100644 index 000000000..672414fb4 --- /dev/null +++ b/lib/concurrent/concern/obligation.rb @@ -0,0 +1,226 @@ +require 'thread' +require 'timeout' + +require 'concurrent/concern/dereferenceable' +require 'concurrent/atomic/event' + +module Concurrent + module Concern + + module Obligation + include Dereferenceable + + # Has the obligation been fulfilled? + # + # @return [Boolean] + def fulfilled? + state == :fulfilled + end + alias_method :realized?, :fulfilled? + + # Has the obligation been rejected? + # + # @return [Boolean] + def rejected? + state == :rejected + end + + # Is obligation completion still pending? + # + # @return [Boolean] + def pending? + state == :pending + end + + # Is the obligation still unscheduled? + # + # @return [Boolean] + def unscheduled? + state == :unscheduled + end + + # Has the obligation completed processing? + # + # @return [Boolean] + def complete? + [:fulfilled, :rejected].include? state + end + + # Has the obligation completed processing? + # + # @return [Boolean] + # + # @deprecated + def completed? + warn '[DEPRECATED] Use #complete? instead' + complete? + end + + # Is the obligation still awaiting completion of processing? + # + # @return [Boolean] + def incomplete? + ! complete? + end + + # The current value of the obligation. Will be `nil` while the state is + # pending or the operation has been rejected. + # + # @param [Numeric] timeout the maximum time in seconds to wait. + # @return [Object] see Dereferenceable#deref + def value(timeout = nil) + wait timeout + deref + end + + # Wait until obligation is complete or the timeout has been reached. + # + # @param [Numeric] timeout the maximum time in seconds to wait. + # @return [Obligation] self + def wait(timeout = nil) + event.wait(timeout) if timeout != 0 && incomplete? + self + end + + # Wait until obligation is complete or the timeout is reached. Will re-raise + # any exceptions raised during processing (but will not raise an exception + # on timeout). + # + # @param [Numeric] timeout the maximum time in seconds to wait. + # @return [Obligation] self + # @raise [Exception] raises the reason when rejected + def wait!(timeout = nil) + wait(timeout).tap { raise self if rejected? } + end + alias_method :no_error!, :wait! + + # The current value of the obligation. Will be `nil` while the state is + # pending or the operation has been rejected. Will re-raise any exceptions + # raised during processing (but will not raise an exception on timeout). + # + # @param [Numeric] timeout the maximum time in seconds to wait. + # @return [Object] see Dereferenceable#deref + # @raise [Exception] raises the reason when rejected + def value!(timeout = nil) + wait(timeout) + if rejected? + raise self + else + deref + end + end + + # The current state of the obligation. + # + # @return [Symbol] the current state + def state + mutex.synchronize { @state } + end + + # If an exception was raised during processing this will return the + # exception object. Will return `nil` when the state is pending or if + # the obligation has been successfully fulfilled. + # + # @return [Exception] the exception raised during processing or `nil` + def reason + mutex.synchronize { @reason } + end + + # @example allows Obligation to be risen + # rejected_ivar = Ivar.new.fail + # raise rejected_ivar + def exception(*args) + raise 'obligation is not rejected' unless rejected? + reason.exception(*args) + end + + protected + + # @!visibility private + def get_arguments_from(opts = {}) + [*opts.fetch(:args, [])] + end + + # @!visibility private + def init_obligation(*args) + init_mutex(*args) + @event = Event.new + end + + # @!visibility private + def event + @event + end + + # @!visibility private + def set_state(success, value, reason) + if success + @value = value + @state = :fulfilled + else + @reason = reason + @state = :rejected + end + end + + # @!visibility private + def state=(value) + mutex.synchronize { ns_set_state(value) } + end + + # Atomic compare and set operation + # State is set to `next_state` only if `current state == expected_current`. + # + # @param [Symbol] next_state + # @param [Symbol] expected_current + # + # @return [Boolean] true is state is changed, false otherwise + # + # @!visibility private + def compare_and_set_state(next_state, *expected_current) + mutex.synchronize do + if expected_current.include? @state + @state = next_state + true + else + false + end + end + end + + # Executes the block within mutex if current state is included in expected_states + # + # @return block value if executed, false otherwise + # + # @!visibility private + def if_state(*expected_states) + mutex.synchronize do + raise ArgumentError.new('no block given') unless block_given? + + if expected_states.include? @state + yield + else + false + end + end + end + + protected + + # Am I in the current state? + # + # @param [Symbol] expected The state to check against + # @return [Boolean] true if in the expected state else false + # + # @!visibility private + def ns_check_state?(expected) + @state == expected + end + + # @!visibility private + def ns_set_state(value) + @state = value + end + end + end +end diff --git a/lib/concurrent/concern/observable.rb b/lib/concurrent/concern/observable.rb new file mode 100644 index 000000000..5069c187f --- /dev/null +++ b/lib/concurrent/concern/observable.rb @@ -0,0 +1,85 @@ +require 'concurrent/atomic/copy_on_notify_observer_set' +require 'concurrent/atomic/copy_on_write_observer_set' + +module Concurrent + module Concern + + # The [observer pattern](http://en.wikipedia.org/wiki/Observer_pattern) is one + # of the most useful design patterns. + # + # The workflow is very simple: + # - an `observer` can register itself to a `subject` via a callback + # - many `observers` can be registered to the same `subject` + # - the `subject` notifies all registered observers when its status changes + # - an `observer` can deregister itself when is no more interested to receive + # event notifications + # + # In a single threaded environment the whole pattern is very easy: the + # `subject` can use a simple data structure to manage all its subscribed + # `observer`s and every `observer` can react directly to every event without + # caring about synchronization. + # + # In a multi threaded environment things are more complex. The `subject` must + # synchronize the access to its data structure and to do so currently we're + # using two specialized ObserverSet: CopyOnWriteObserverSet and + # CopyOnNotifyObserverSet. + # + # When implementing and `observer` there's a very important rule to remember: + # **there are no guarantees about the thread that will execute the callback** + # + # Let's take this example + # ``` + # class Observer + # def initialize + # @count = 0 + # end + # + # def update + # @count += 1 + # end + # end + # + # obs = Observer.new + # [obj1, obj2, obj3, obj4].each { |o| o.add_observer(obs) } + # # execute [obj1, obj2, obj3, obj4] + # ``` + # + # `obs` is wrong because the variable `@count` can be accessed by different + # threads at the same time, so it should be synchronized (using either a Mutex + # or an AtomicFixum) + module Observable + + # @return [Object] the added observer + def add_observer(*args, &block) + observers.add_observer(*args, &block) + end + + # as #add_observer but it can be used for chaining + # @return [Observable] self + def with_observer(*args, &block) + add_observer(*args, &block) + self + end + + # @return [Object] the deleted observer + def delete_observer(*args) + observers.delete_observer(*args) + end + + # @return [Observable] self + def delete_observers + observers.delete_observers + self + end + + # @return [Integer] the observers count + def count_observers + observers.count_observers + end + + protected + + attr_accessor :observers + end + end +end diff --git a/lib/concurrent/configuration.rb b/lib/concurrent/configuration.rb index 7041fd46c..ec926deb6 100644 --- a/lib/concurrent/configuration.rb +++ b/lib/concurrent/configuration.rb @@ -3,10 +3,11 @@ require 'concurrent/errors' require 'concurrent/at_exit' require 'concurrent/executors' +require 'concurrent/concern/logging' require 'concurrent/utility/processor_count' module Concurrent - extend Logging + extend Concern::Logging # Suppresses all output when used for logging. NULL_LOGGER = lambda { |level, progname, message = nil, &block| } diff --git a/lib/concurrent/delay.rb b/lib/concurrent/delay.rb index e9c8d31ce..481ec0b43 100644 --- a/lib/concurrent/delay.rb +++ b/lib/concurrent/delay.rb @@ -1,6 +1,6 @@ require 'thread' require 'concurrent/configuration' -require 'concurrent/obligation' +require 'concurrent/concern/obligation' require 'concurrent/executor/executor' require 'concurrent/executor/immediate_executor' require 'concurrent/synchronization_object' @@ -39,7 +39,7 @@ module Concurrent # # @see Concurrent::Dereferenceable class Delay < SynchronizationObject - include Obligation + include Concern::Obligation # NOTE: Because the global thread pools are lazy-loaded with these objects # there is a performance hit every time we post a new task to one of these diff --git a/lib/concurrent/dereferenceable.rb b/lib/concurrent/dereferenceable.rb deleted file mode 100644 index 6a9cf4a6c..000000000 --- a/lib/concurrent/dereferenceable.rb +++ /dev/null @@ -1,106 +0,0 @@ -module Concurrent - - # Object references in Ruby are mutable. This can lead to serious problems when - # the `#value` of a concurrent object is a mutable reference. Which is always the - # case unless the value is a `Fixnum`, `Symbol`, or similar "primitive" data type. - # Most classes in this library that expose a `#value` getter method do so using the - # `Dereferenceable` mixin module. - # - # Objects with this mixin can be configured with a few options that can help protect - # the program from potentially dangerous operations. - # - # * `:dup_on_deref` when true will call the `#dup` method on the `value` - # object every time the `#value` method is called (default: false) - # * `:freeze_on_deref` when true will call the `#freeze` method on the `value` object - # every time the `#value` method is called (default: false) - # * `:copy_on_deref` when given a `Proc` object the `Proc` will be run every - # time the `#value` method is called. The `Proc` will be given the current - # `value` as its only parameter and the result returned by the block will - # be the return value of the `#value` call. When `nil` this option will be - # ignored (default: nil) - module Dereferenceable - - # Return the value this object represents after applying the options specified - # by the `#set_deref_options` method. - # - # When multiple deref options are set the order of operations is strictly defined. - # The order of deref operations is: - # * `:copy_on_deref` - # * `:dup_on_deref` - # * `:freeze_on_deref` - # - # Because of this ordering there is no need to `#freeze` an object created by a - # provided `:copy_on_deref` block. Simply set `:freeze_on_deref` to `true`. - # Setting both `:dup_on_deref` to `true` and `:freeze_on_deref` to `true` is - # as close to the behavior of a "pure" functional language (like Erlang, Clojure, - # or Haskell) as we are likely to get in Ruby. - # - # This method is thread-safe and synchronized with the internal `#mutex`. - # - # @return [Object] the current value of the object - def value - mutex.synchronize { apply_deref_options(@value) } - end - alias_method :deref, :value - - protected - - # Set the internal value of this object - # - # @param [Object] value the new value - def value=(value) - mutex.synchronize{ @value = value } - end - - # A mutex lock used for synchronizing thread-safe operations. Methods defined - # by `Dereferenceable` are synchronized using the `Mutex` returned from this - # method. Operations performed by the including class that operate on the - # `@value` instance variable should be locked with this `Mutex`. - # - # @return [Mutex] the synchronization object - def mutex - @mutex - end - - # Initializes the internal `Mutex`. - # - # @note This method *must* be called from within the constructor of the including class. - # - # @see #mutex - def init_mutex(mutex = Mutex.new) - @mutex = mutex - end - - # Set the options which define the operations #value performs before - # returning data to the caller (dereferencing). - # - # @note Most classes that include this module will call `#set_deref_options` - # from within the constructor, thus allowing these options to be set at - # object creation. - # - # @param [Hash] opts the options defining dereference behavior. - # @option opts [String] :dup_on_deref (false) call `#dup` before returning the data - # @option opts [String] :freeze_on_deref (false) call `#freeze` before returning the data - # @option opts [String] :copy_on_deref (nil) call the given `Proc` passing - # the internal value and returning the value returned from the proc - def set_deref_options(opts = {}) - mutex.synchronize do - @dup_on_deref = opts[:dup_on_deref] || opts[:dup] - @freeze_on_deref = opts[:freeze_on_deref] || opts[:freeze] - @copy_on_deref = opts[:copy_on_deref] || opts[:copy] - @do_nothing_on_deref = !(@dup_on_deref || @freeze_on_deref || @copy_on_deref) - nil - end - end - - # @!visibility private - def apply_deref_options(value) # :nodoc: - return nil if value.nil? - return value if @do_nothing_on_deref - value = @copy_on_deref.call(value) if @copy_on_deref - value = value.dup if @dup_on_deref - value = value.freeze if @freeze_on_deref - value - end - end -end diff --git a/lib/concurrent/executor/executor_service.rb b/lib/concurrent/executor/executor_service.rb index ddc248797..3330166f8 100644 --- a/lib/concurrent/executor/executor_service.rb +++ b/lib/concurrent/executor/executor_service.rb @@ -1,5 +1,5 @@ require 'concurrent/errors' -require 'concurrent/logging' +require 'concurrent/concern/logging' require 'concurrent/at_exit' require 'concurrent/atomic/event' require 'concurrent/synchronization_object' @@ -7,7 +7,7 @@ module Concurrent module ExecutorService - include Logging + include Concern::Logging # @!macro [attach] executor_service_method_post # diff --git a/lib/concurrent/executor/ruby_thread_pool_executor.rb b/lib/concurrent/executor/ruby_thread_pool_executor.rb index 7f5d70ad0..e97c01b8d 100644 --- a/lib/concurrent/executor/ruby_thread_pool_executor.rb +++ b/lib/concurrent/executor/ruby_thread_pool_executor.rb @@ -1,6 +1,7 @@ require 'thread' require 'concurrent/atomic/event' +require 'concurrent/concern/logging' require 'concurrent/executor/executor_service' require 'concurrent/utility/monotonic_time' @@ -298,7 +299,7 @@ def ns_prune_pool end class Worker - include Logging + include Concern::Logging def initialize(pool) # instance variables accessed only under pool's lock so no need to sync here again diff --git a/lib/concurrent/executor/serialized_execution.rb b/lib/concurrent/executor/serialized_execution.rb index 0d8b6ad1d..3091650b3 100644 --- a/lib/concurrent/executor/serialized_execution.rb +++ b/lib/concurrent/executor/serialized_execution.rb @@ -1,13 +1,13 @@ require 'delegate' require 'concurrent/executor/executor_service' -require 'concurrent/logging' +require 'concurrent/concern/logging' require 'concurrent/synchronization_object' module Concurrent # Ensures passed jobs in a serialized order never running at the same time. class SerializedExecution < SynchronizationObject - include Logging + include Concern::Logging Job = Struct.new(:executor, :args, :block) do def call diff --git a/lib/concurrent/ivar.rb b/lib/concurrent/ivar.rb index 43c41ab68..746fed0a6 100644 --- a/lib/concurrent/ivar.rb +++ b/lib/concurrent/ivar.rb @@ -1,8 +1,8 @@ require 'thread' require 'concurrent/errors' -require 'concurrent/obligation' -require 'concurrent/observable' +require 'concurrent/concern/obligation' +require 'concurrent/concern/observable' require 'concurrent/synchronization_object' module Concurrent @@ -40,8 +40,8 @@ module Concurrent # ivar.get #=> 14 # ivar.set 2 # would now be an error class IVar < SynchronizationObject - include Obligation - include Observable + include Concern::Obligation + include Concern::Observable # @!visibility private NO_VALUE = Object.new # :nodoc: diff --git a/lib/concurrent/logging.rb b/lib/concurrent/logging.rb deleted file mode 100644 index 99658c3a1..000000000 --- a/lib/concurrent/logging.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'logger' - -module Concurrent - # Include where logging is needed - module Logging - include Logger::Severity - - # Logs through {Configuration#logger}, it can be overridden by setting @logger - # @param [Integer] level one of Logger::Severity constants - # @param [String] progname e.g. a path of an Actor - # @param [String, nil] message when nil block is used to generate the message - # @yieldreturn [String] a message - def log(level, progname, message = nil, &block) - (@logger || Concurrent.global_logger).call level, progname, message, &block - rescue => error - $stderr.puts "`Concurrent.configuration.logger` failed to log #{[level, progname, message, block]}\n" + - "#{error.message} (#{error.class})\n#{error.backtrace.join "\n"}" - end - end -end diff --git a/lib/concurrent/mvar.rb b/lib/concurrent/mvar.rb index 1f486e3c2..ff04b9864 100644 --- a/lib/concurrent/mvar.rb +++ b/lib/concurrent/mvar.rb @@ -1,4 +1,4 @@ -require 'concurrent/dereferenceable' +require 'concurrent/concern/dereferenceable' module Concurrent @@ -33,7 +33,7 @@ module Concurrent # (PoPL), 1996. class MVar - include Dereferenceable + include Concern::Dereferenceable # Unique value that represents that an `MVar` was empty EMPTY = Object.new diff --git a/lib/concurrent/obligation.rb b/lib/concurrent/obligation.rb deleted file mode 100644 index 3bd29f594..000000000 --- a/lib/concurrent/obligation.rb +++ /dev/null @@ -1,224 +0,0 @@ -require 'thread' -require 'timeout' - -require 'concurrent/dereferenceable' -require 'concurrent/atomic/event' - -module Concurrent - - module Obligation - include Dereferenceable - - # Has the obligation been fulfilled? - # - # @return [Boolean] - def fulfilled? - state == :fulfilled - end - alias_method :realized?, :fulfilled? - - # Has the obligation been rejected? - # - # @return [Boolean] - def rejected? - state == :rejected - end - - # Is obligation completion still pending? - # - # @return [Boolean] - def pending? - state == :pending - end - - # Is the obligation still unscheduled? - # - # @return [Boolean] - def unscheduled? - state == :unscheduled - end - - # Has the obligation completed processing? - # - # @return [Boolean] - def complete? - [:fulfilled, :rejected].include? state - end - - # Has the obligation completed processing? - # - # @return [Boolean] - # - # @deprecated - def completed? - warn '[DEPRECATED] Use #complete? instead' - complete? - end - - # Is the obligation still awaiting completion of processing? - # - # @return [Boolean] - def incomplete? - ! complete? - end - - # The current value of the obligation. Will be `nil` while the state is - # pending or the operation has been rejected. - # - # @param [Numeric] timeout the maximum time in seconds to wait. - # @return [Object] see Dereferenceable#deref - def value(timeout = nil) - wait timeout - deref - end - - # Wait until obligation is complete or the timeout has been reached. - # - # @param [Numeric] timeout the maximum time in seconds to wait. - # @return [Obligation] self - def wait(timeout = nil) - event.wait(timeout) if timeout != 0 && incomplete? - self - end - - # Wait until obligation is complete or the timeout is reached. Will re-raise - # any exceptions raised during processing (but will not raise an exception - # on timeout). - # - # @param [Numeric] timeout the maximum time in seconds to wait. - # @return [Obligation] self - # @raise [Exception] raises the reason when rejected - def wait!(timeout = nil) - wait(timeout).tap { raise self if rejected? } - end - alias_method :no_error!, :wait! - - # The current value of the obligation. Will be `nil` while the state is - # pending or the operation has been rejected. Will re-raise any exceptions - # raised during processing (but will not raise an exception on timeout). - # - # @param [Numeric] timeout the maximum time in seconds to wait. - # @return [Object] see Dereferenceable#deref - # @raise [Exception] raises the reason when rejected - def value!(timeout = nil) - wait(timeout) - if rejected? - raise self - else - deref - end - end - - # The current state of the obligation. - # - # @return [Symbol] the current state - def state - mutex.synchronize { @state } - end - - # If an exception was raised during processing this will return the - # exception object. Will return `nil` when the state is pending or if - # the obligation has been successfully fulfilled. - # - # @return [Exception] the exception raised during processing or `nil` - def reason - mutex.synchronize { @reason } - end - - # @example allows Obligation to be risen - # rejected_ivar = Ivar.new.fail - # raise rejected_ivar - def exception(*args) - raise 'obligation is not rejected' unless rejected? - reason.exception(*args) - end - - protected - - # @!visibility private - def get_arguments_from(opts = {}) - [*opts.fetch(:args, [])] - end - - # @!visibility private - def init_obligation(*args) - init_mutex(*args) - @event = Event.new - end - - # @!visibility private - def event - @event - end - - # @!visibility private - def set_state(success, value, reason) - if success - @value = value - @state = :fulfilled - else - @reason = reason - @state = :rejected - end - end - - # @!visibility private - def state=(value) - mutex.synchronize { ns_set_state(value) } - end - - # Atomic compare and set operation - # State is set to `next_state` only if `current state == expected_current`. - # - # @param [Symbol] next_state - # @param [Symbol] expected_current - # - # @return [Boolean] true is state is changed, false otherwise - # - # @!visibility private - def compare_and_set_state(next_state, *expected_current) - mutex.synchronize do - if expected_current.include? @state - @state = next_state - true - else - false - end - end - end - - # Executes the block within mutex if current state is included in expected_states - # - # @return block value if executed, false otherwise - # - # @!visibility private - def if_state(*expected_states) - mutex.synchronize do - raise ArgumentError.new('no block given') unless block_given? - - if expected_states.include? @state - yield - else - false - end - end - end - - protected - - # Am I in the current state? - # - # @param [Symbol] expected The state to check against - # @return [Boolean] true if in the expected state else false - # - # @!visibility private - def ns_check_state?(expected) - @state == expected - end - - # @!visibility private - def ns_set_state(value) - @state = value - end - end -end diff --git a/lib/concurrent/observable.rb b/lib/concurrent/observable.rb deleted file mode 100644 index eecc833ef..000000000 --- a/lib/concurrent/observable.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'concurrent/atomic/copy_on_notify_observer_set' -require 'concurrent/atomic/copy_on_write_observer_set' - -module Concurrent - - # The [observer pattern](http://en.wikipedia.org/wiki/Observer_pattern) is one - # of the most useful design patterns. - # - # The workflow is very simple: - # - an `observer` can register itself to a `subject` via a callback - # - many `observers` can be registered to the same `subject` - # - the `subject` notifies all registered observers when its status changes - # - an `observer` can deregister itself when is no more interested to receive - # event notifications - # - # In a single threaded environment the whole pattern is very easy: the - # `subject` can use a simple data structure to manage all its subscribed - # `observer`s and every `observer` can react directly to every event without - # caring about synchronization. - # - # In a multi threaded environment things are more complex. The `subject` must - # synchronize the access to its data structure and to do so currently we're - # using two specialized ObserverSet: CopyOnWriteObserverSet and - # CopyOnNotifyObserverSet. - # - # When implementing and `observer` there's a very important rule to remember: - # **there are no guarantees about the thread that will execute the callback** - # - # Let's take this example - # ``` - # class Observer - # def initialize - # @count = 0 - # end - # - # def update - # @count += 1 - # end - # end - # - # obs = Observer.new - # [obj1, obj2, obj3, obj4].each { |o| o.add_observer(obs) } - # # execute [obj1, obj2, obj3, obj4] - # ``` - # - # `obs` is wrong because the variable `@count` can be accessed by different - # threads at the same time, so it should be synchronized (using either a Mutex - # or an AtomicFixum) - module Observable - - # @return [Object] the added observer - def add_observer(*args, &block) - observers.add_observer(*args, &block) - end - - # as #add_observer but it can be used for chaining - # @return [Observable] self - def with_observer(*args, &block) - add_observer(*args, &block) - self - end - - # @return [Object] the deleted observer - def delete_observer(*args) - observers.delete_observer(*args) - end - - # @return [Observable] self - def delete_observers - observers.delete_observers - self - end - - # @return [Integer] the observers count - def count_observers - observers.count_observers - end - - protected - - attr_accessor :observers - end -end diff --git a/lib/concurrent/promise.rb b/lib/concurrent/promise.rb index b21cb1ebb..ec67a669d 100644 --- a/lib/concurrent/promise.rb +++ b/lib/concurrent/promise.rb @@ -1,7 +1,6 @@ require 'thread' require 'concurrent/errors' require 'concurrent/ivar' -require 'concurrent/obligation' require 'concurrent/executor/executor' module Concurrent diff --git a/lib/concurrent/timer_task.rb b/lib/concurrent/timer_task.rb index 6566ea345..7022deeb6 100644 --- a/lib/concurrent/timer_task.rb +++ b/lib/concurrent/timer_task.rb @@ -1,5 +1,5 @@ -require 'concurrent/dereferenceable' -require 'concurrent/observable' +require 'concurrent/concern/dereferenceable' +require 'concurrent/concern/observable' require 'concurrent/atomic/atomic_boolean' require 'concurrent/executor/executor_service' require 'concurrent/executor/safe_task_executor' @@ -148,8 +148,8 @@ module Concurrent # @see http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html # @see http://docs.oracle.com/javase/7/docs/api/java/util/TimerTask.html class TimerTask < RubyExecutorService - include Dereferenceable - include Observable + include Concern::Dereferenceable + include Concern::Observable # Default `:execution_interval` in seconds. EXECUTION_INTERVAL = 60 diff --git a/spec/concurrent/agent_spec.rb b/spec/concurrent/agent_spec.rb index cc3b2df4c..e9d858e30 100644 --- a/spec/concurrent/agent_spec.rb +++ b/spec/concurrent/agent_spec.rb @@ -1,5 +1,5 @@ -require_relative 'dereferenceable_shared' -require_relative 'observable_shared' +require_relative 'concern/dereferenceable_shared' +require_relative 'concern/observable_shared' module Concurrent diff --git a/spec/concurrent/channel/probe_spec.rb b/spec/concurrent/channel/probe_spec.rb index bac8ab46d..817a3053f 100644 --- a/spec/concurrent/channel/probe_spec.rb +++ b/spec/concurrent/channel/probe_spec.rb @@ -1,4 +1,4 @@ -require_relative '../observable_shared' +require_relative '../concern/observable_shared' module Concurrent diff --git a/spec/concurrent/dereferenceable_shared.rb b/spec/concurrent/concern/dereferenceable_shared.rb similarity index 100% rename from spec/concurrent/dereferenceable_shared.rb rename to spec/concurrent/concern/dereferenceable_shared.rb diff --git a/spec/concurrent/obligation_shared.rb b/spec/concurrent/concern/obligation_shared.rb similarity index 100% rename from spec/concurrent/obligation_shared.rb rename to spec/concurrent/concern/obligation_shared.rb diff --git a/spec/concurrent/concern/obligation_spec.rb b/spec/concurrent/concern/obligation_spec.rb new file mode 100644 index 000000000..0366d506a --- /dev/null +++ b/spec/concurrent/concern/obligation_spec.rb @@ -0,0 +1,330 @@ +module Concurrent + module Concern + + describe Obligation do + + let (:obligation_class) do + + Class.new do + include Obligation + + def initialize + init_mutex + end + + public :state=, :compare_and_set_state, :if_state, :mutex + attr_writer :value, :reason + end + end + + let (:obligation) { obligation_class.new } + let (:event) { double 'event' } + + shared_examples :incomplete do + + it 'should be not completed' do + expect(obligation).not_to be_complete + end + + it 'should be incomplete' do + expect(obligation).to be_incomplete + end + + methods = [:value, :value!, :no_error!] + methods.each do |method| + describe "##{method}" do + + it 'should return immediately if timeout is zero' do + result = obligation.send(method, 0) + if method == :no_error! + expect(result).to eq obligation + else + expect(result).to be_nil + end + end + + it 'should block on the event if timeout is not set' do + allow(obligation).to receive(:event).and_return(event) + expect(event).to receive(:wait).with(nil) + + obligation.send method + end + + it 'should block on the event if timeout is not zero' do + allow(obligation).to receive(:event).and_return(event) + expect(event).to receive(:wait).with(5) + + obligation.send(method, 5) + end + + end + end + end + + context 'unscheduled' do + before(:each) { obligation.state = :unscheduled } + it_should_behave_like :incomplete + end + + context 'pending' do + before(:each) { obligation.state = :pending } + it_should_behave_like :incomplete + end + + context 'fulfilled' do + + before(:each) do + obligation.state = :fulfilled + obligation.send(:value=, 42) + allow(obligation).to receive(:event).and_return(event) + end + + it 'should be completed' do + expect(obligation).to be_complete + end + + it 'should be not incomplete' do + expect(obligation).not_to be_incomplete + end + + describe '#value' do + + it 'should return immediately if timeout is zero' do + expect(obligation.value(0)).to eq 42 + end + + it 'should return immediately if timeout is not set' do + expect(event).not_to receive(:wait) + + expect(obligation.value).to eq 42 + end + + it 'should return immediately if timeout is not zero' do + expect(event).not_to receive(:wait) + + expect(obligation.value(5)).to eq 42 + end + + end + + describe '#value!' do + + it 'should return immediately if timeout is zero' do + expect(obligation.value!(0)).to eq 42 + end + + it 'should return immediately if timeout is not set' do + expect(event).not_to receive(:wait) + + expect(obligation.value!).to eq 42 + end + + it 'should return immediately if timeout is not zero' do + expect(event).not_to receive(:wait) + + expect(obligation.value!(5)).to eq 42 + end + + end + + describe '#no_error!' do + + it 'should return immediately if timeout is zero' do + expect(obligation.no_error!(0)).to eq obligation + end + + it 'should return immediately if timeout is not set' do + expect(event).not_to receive(:wait) + + expect(obligation.no_error!).to eq obligation + end + + it 'should return immediately if timeout is not zero' do + expect(event).not_to receive(:wait) + + expect(obligation.no_error!(5)).to eq obligation + end + + end + + end + + context 'rejected' do + + before(:each) do + obligation.state = :rejected + allow(obligation).to receive(:event).and_return(event) + end + + it 'should be completed' do + expect(obligation).to be_complete + end + + it 'should be not incomplete' do + expect(obligation).not_to be_incomplete + end + + + describe '#value' do + + it 'should return immediately if timeout is zero' do + expect(event).not_to receive(:wait) + + expect(obligation.value(0)).to be_nil + end + + it 'should return immediately if timeout is not set' do + expect(event).not_to receive(:wait) + + expect(obligation.value).to be_nil + end + + it 'should return immediately if timeout is not zero' do + expect(event).not_to receive(:wait) + + expect(obligation.value(5)).to be_nil + end + + end + + describe '#value!' do + + it 'should return immediately if timeout is zero' do + expect(event).not_to receive(:wait) + + expect { obligation.value!(0) }.to raise_error + end + + it 'should return immediately if timeout is not set' do + expect(event).not_to receive(:wait) + + expect { obligation.value! }.to raise_error + end + + it 'should return immediately if timeout is not zero' do + expect(event).not_to receive(:wait) + + expect { obligation.value!(5) }.to raise_error + end + + end + + describe '#no_error!' do + + it 'should return immediately if timeout is zero' do + expect(event).not_to receive(:wait) + + expect { obligation.no_error!(0) }.to raise_error + end + + it 'should return immediately if timeout is not set' do + expect(event).not_to receive(:wait) + + expect { obligation.no_error! }.to raise_error + end + + it 'should return immediately if timeout is not zero' do + expect(event).not_to receive(:wait) + + expect { obligation.no_error!(5) }.to raise_error + end + + end + + end + + describe '#compare_and_set_state' do + + before(:each) { obligation.state = :unscheduled } + + context 'unexpected state' do + it 'should return false if state is not the expected one' do + expect(obligation.compare_and_set_state(:pending, :rejected)).to be_falsey + end + + it 'should not change the state if current is not the expected one' do + obligation.compare_and_set_state(:pending, :rejected) + expect(obligation.state).to eq :unscheduled + end + end + + context 'expected state' do + it 'should return true if state is the expected one' do + expect(obligation.compare_and_set_state(:pending, :unscheduled)).to be_truthy + end + + it 'should not change the state if current is not the expected one' do + obligation.compare_and_set_state(:pending, :unscheduled) + expect(obligation.state).to eq :pending + end + end + + end + + describe '#if_state' do + + before(:each) { obligation.state = :unscheduled } + + it 'should raise without block' do + expect { obligation.if_state(:pending) }.to raise_error(ArgumentError) + end + + it 'should return false if state is not expected' do + expect(obligation.if_state(:pending, :rejected) { 42 }).to be_falsey + end + + it 'should the block value if state is expected' do + expect(obligation.if_state(:rejected, :unscheduled) { 42 }).to eq 42 + end + + it 'should execute the block within the mutex' do + obligation.if_state(:unscheduled) { expect(obligation.mutex).to be_locked } + end + end + + context '#get_arguments_from' do + + it 'returns an empty array when opts is not given' do + args = obligation.send(:get_arguments_from) + expect(args).to be_a Array + expect(args).to be_empty + end + + it 'returns an empty array when opts is an empty hash' do + args = obligation.send(:get_arguments_from, {}) + expect(args).to be_a Array + expect(args).to be_empty + end + + it 'returns an empty array when there is no :args key' do + args = obligation.send(:get_arguments_from, foo: 'bar') + expect(args).to be_a Array + expect(args).to be_empty + end + + it 'returns an empty array when the :args key has a nil value' do + args = obligation.send(:get_arguments_from, args: nil) + expect(args).to be_a Array + expect(args).to be_empty + end + + it 'returns a one-element array when the :args key has a non-array value' do + args = obligation.send(:get_arguments_from, args: 'foo') + expect(args).to eq ['foo'] + end + + it 'returns an array when when the :args key has an array value' do + expected = [1, 2, 3, 4] + args = obligation.send(:get_arguments_from, args: expected) + expect(args).to eq expected + end + + it 'returns the given array when the :args key has a complex array value' do + expected = [(1..10).to_a, (20..30).to_a, (100..110).to_a] + args = obligation.send(:get_arguments_from, args: expected) + expect(args).to eq expected + end + end + end + end +end diff --git a/spec/concurrent/observable_shared.rb b/spec/concurrent/concern/observable_shared.rb similarity index 100% rename from spec/concurrent/observable_shared.rb rename to spec/concurrent/concern/observable_shared.rb diff --git a/spec/concurrent/concern/observable_spec.rb b/spec/concurrent/concern/observable_spec.rb new file mode 100644 index 000000000..115cdf565 --- /dev/null +++ b/spec/concurrent/concern/observable_spec.rb @@ -0,0 +1,56 @@ +module Concurrent + module Concern + + describe Observable do + + let (:described_class) do + Class.new do + include Concurrent::Concern::Observable + public :observers, :observers= + end + end + + let(:observer_set) { double(:observer_set) } + subject { described_class.new } + + before(:each) do + subject.observers = observer_set + end + + it 'does not initialize set by by default' do + expect(described_class.new.observers).to be_nil + end + + it 'uses the given observer set' do + expected = CopyOnWriteObserverSet.new + subject.observers = expected + expect(subject.observers).to eql expected + end + + it 'delegates #add_observer' do + expect(observer_set).to receive(:add_observer).with(:observer) { |v| v } + expect(subject.add_observer(:observer)).to eq :observer + end + + it 'delegates #with_observer' do + expect(observer_set).to receive(:add_observer).with(:observer) { |v| v } + expect(subject.with_observer(:observer)).to eq subject + end + + it 'delegates #delete_observer' do + expect(observer_set).to receive(:delete_observer).with(:observer) + subject.delete_observer(:observer) + end + + it 'delegates #delete_observers' do + expect(observer_set).to receive(:delete_observers).with(no_args) + subject.delete_observers + end + + it 'delegates #count_observers' do + expect(observer_set).to receive(:count_observers).with(no_args) + subject.count_observers + end + end + end +end diff --git a/spec/concurrent/delay_spec.rb b/spec/concurrent/delay_spec.rb index eb3362a44..e94c5c5ff 100644 --- a/spec/concurrent/delay_spec.rb +++ b/spec/concurrent/delay_spec.rb @@ -1,5 +1,5 @@ -require_relative 'dereferenceable_shared' -require_relative 'obligation_shared' +require_relative 'concern/dereferenceable_shared' +require_relative 'concern/obligation_shared' module Concurrent diff --git a/spec/concurrent/ivar_shared.rb b/spec/concurrent/ivar_shared.rb index b22520fab..69ca72905 100644 --- a/spec/concurrent/ivar_shared.rb +++ b/spec/concurrent/ivar_shared.rb @@ -1,6 +1,6 @@ -require_relative 'dereferenceable_shared' -require_relative 'obligation_shared' -require_relative 'observable_shared' +require_relative 'concern/dereferenceable_shared' +require_relative 'concern/obligation_shared' +require_relative 'concern/observable_shared' shared_examples :ivar do diff --git a/spec/concurrent/mvar_spec.rb b/spec/concurrent/mvar_spec.rb index d2de89130..5edeacb15 100644 --- a/spec/concurrent/mvar_spec.rb +++ b/spec/concurrent/mvar_spec.rb @@ -1,4 +1,4 @@ -require_relative 'dereferenceable_shared' +require_relative 'concern/dereferenceable_shared' module Concurrent diff --git a/spec/concurrent/obligation_spec.rb b/spec/concurrent/obligation_spec.rb deleted file mode 100644 index d909ff0f7..000000000 --- a/spec/concurrent/obligation_spec.rb +++ /dev/null @@ -1,328 +0,0 @@ -module Concurrent - - describe Obligation do - - let (:obligation_class) do - - Class.new do - include Obligation - - def initialize - init_mutex - end - - public :state=, :compare_and_set_state, :if_state, :mutex - attr_writer :value, :reason - end - end - - let (:obligation) { obligation_class.new } - let (:event) { double 'event' } - - shared_examples :incomplete do - - it 'should be not completed' do - expect(obligation).not_to be_complete - end - - it 'should be incomplete' do - expect(obligation).to be_incomplete - end - - methods = [:value, :value!, :no_error!] - methods.each do |method| - describe "##{method}" do - - it 'should return immediately if timeout is zero' do - result = obligation.send(method, 0) - if method == :no_error! - expect(result).to eq obligation - else - expect(result).to be_nil - end - end - - it 'should block on the event if timeout is not set' do - allow(obligation).to receive(:event).and_return(event) - expect(event).to receive(:wait).with(nil) - - obligation.send method - end - - it 'should block on the event if timeout is not zero' do - allow(obligation).to receive(:event).and_return(event) - expect(event).to receive(:wait).with(5) - - obligation.send(method, 5) - end - - end - end - end - - context 'unscheduled' do - before(:each) { obligation.state = :unscheduled } - it_should_behave_like :incomplete - end - - context 'pending' do - before(:each) { obligation.state = :pending } - it_should_behave_like :incomplete - end - - context 'fulfilled' do - - before(:each) do - obligation.state = :fulfilled - obligation.send(:value=, 42) - allow(obligation).to receive(:event).and_return(event) - end - - it 'should be completed' do - expect(obligation).to be_complete - end - - it 'should be not incomplete' do - expect(obligation).not_to be_incomplete - end - - describe '#value' do - - it 'should return immediately if timeout is zero' do - expect(obligation.value(0)).to eq 42 - end - - it 'should return immediately if timeout is not set' do - expect(event).not_to receive(:wait) - - expect(obligation.value).to eq 42 - end - - it 'should return immediately if timeout is not zero' do - expect(event).not_to receive(:wait) - - expect(obligation.value(5)).to eq 42 - end - - end - - describe '#value!' do - - it 'should return immediately if timeout is zero' do - expect(obligation.value!(0)).to eq 42 - end - - it 'should return immediately if timeout is not set' do - expect(event).not_to receive(:wait) - - expect(obligation.value!).to eq 42 - end - - it 'should return immediately if timeout is not zero' do - expect(event).not_to receive(:wait) - - expect(obligation.value!(5)).to eq 42 - end - - end - - describe '#no_error!' do - - it 'should return immediately if timeout is zero' do - expect(obligation.no_error!(0)).to eq obligation - end - - it 'should return immediately if timeout is not set' do - expect(event).not_to receive(:wait) - - expect(obligation.no_error!).to eq obligation - end - - it 'should return immediately if timeout is not zero' do - expect(event).not_to receive(:wait) - - expect(obligation.no_error!(5)).to eq obligation - end - - end - - end - - context 'rejected' do - - before(:each) do - obligation.state = :rejected - allow(obligation).to receive(:event).and_return(event) - end - - it 'should be completed' do - expect(obligation).to be_complete - end - - it 'should be not incomplete' do - expect(obligation).not_to be_incomplete - end - - - describe '#value' do - - it 'should return immediately if timeout is zero' do - expect(event).not_to receive(:wait) - - expect(obligation.value(0)).to be_nil - end - - it 'should return immediately if timeout is not set' do - expect(event).not_to receive(:wait) - - expect(obligation.value).to be_nil - end - - it 'should return immediately if timeout is not zero' do - expect(event).not_to receive(:wait) - - expect(obligation.value(5)).to be_nil - end - - end - - describe '#value!' do - - it 'should return immediately if timeout is zero' do - expect(event).not_to receive(:wait) - - expect { obligation.value!(0) }.to raise_error - end - - it 'should return immediately if timeout is not set' do - expect(event).not_to receive(:wait) - - expect { obligation.value! }.to raise_error - end - - it 'should return immediately if timeout is not zero' do - expect(event).not_to receive(:wait) - - expect { obligation.value!(5) }.to raise_error - end - - end - - describe '#no_error!' do - - it 'should return immediately if timeout is zero' do - expect(event).not_to receive(:wait) - - expect { obligation.no_error!(0) }.to raise_error - end - - it 'should return immediately if timeout is not set' do - expect(event).not_to receive(:wait) - - expect { obligation.no_error! }.to raise_error - end - - it 'should return immediately if timeout is not zero' do - expect(event).not_to receive(:wait) - - expect { obligation.no_error!(5) }.to raise_error - end - - end - - end - - describe '#compare_and_set_state' do - - before(:each) { obligation.state = :unscheduled } - - context 'unexpected state' do - it 'should return false if state is not the expected one' do - expect(obligation.compare_and_set_state(:pending, :rejected)).to be_falsey - end - - it 'should not change the state if current is not the expected one' do - obligation.compare_and_set_state(:pending, :rejected) - expect(obligation.state).to eq :unscheduled - end - end - - context 'expected state' do - it 'should return true if state is the expected one' do - expect(obligation.compare_and_set_state(:pending, :unscheduled)).to be_truthy - end - - it 'should not change the state if current is not the expected one' do - obligation.compare_and_set_state(:pending, :unscheduled) - expect(obligation.state).to eq :pending - end - end - - end - - describe '#if_state' do - - before(:each) { obligation.state = :unscheduled } - - it 'should raise without block' do - expect { obligation.if_state(:pending) }.to raise_error(ArgumentError) - end - - it 'should return false if state is not expected' do - expect(obligation.if_state(:pending, :rejected) { 42 }).to be_falsey - end - - it 'should the block value if state is expected' do - expect(obligation.if_state(:rejected, :unscheduled) { 42 }).to eq 42 - end - - it 'should execute the block within the mutex' do - obligation.if_state(:unscheduled) { expect(obligation.mutex).to be_locked } - end - end - - context '#get_arguments_from' do - - it 'returns an empty array when opts is not given' do - args = obligation.send(:get_arguments_from) - expect(args).to be_a Array - expect(args).to be_empty - end - - it 'returns an empty array when opts is an empty hash' do - args = obligation.send(:get_arguments_from, {}) - expect(args).to be_a Array - expect(args).to be_empty - end - - it 'returns an empty array when there is no :args key' do - args = obligation.send(:get_arguments_from, foo: 'bar') - expect(args).to be_a Array - expect(args).to be_empty - end - - it 'returns an empty array when the :args key has a nil value' do - args = obligation.send(:get_arguments_from, args: nil) - expect(args).to be_a Array - expect(args).to be_empty - end - - it 'returns a one-element array when the :args key has a non-array value' do - args = obligation.send(:get_arguments_from, args: 'foo') - expect(args).to eq ['foo'] - end - - it 'returns an array when when the :args key has an array value' do - expected = [1, 2, 3, 4] - args = obligation.send(:get_arguments_from, args: expected) - expect(args).to eq expected - end - - it 'returns the given array when the :args key has a complex array value' do - expected = [(1..10).to_a, (20..30).to_a, (100..110).to_a] - args = obligation.send(:get_arguments_from, args: expected) - expect(args).to eq expected - end - end - end -end diff --git a/spec/concurrent/observable_spec.rb b/spec/concurrent/observable_spec.rb deleted file mode 100644 index 0296c5bc7..000000000 --- a/spec/concurrent/observable_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Concurrent - - describe Observable do - - let (:described_class) do - Class.new do - include Concurrent::Observable - public :observers, :observers= - end - end - - let(:observer_set) { double(:observer_set) } - subject { described_class.new } - - before(:each) do - subject.observers = observer_set - end - - it 'does not initialize set by by default' do - expect(described_class.new.observers).to be_nil - end - - it 'uses the given observer set' do - expected = CopyOnWriteObserverSet.new - subject.observers = expected - expect(subject.observers).to eql expected - end - - it 'delegates #add_observer' do - expect(observer_set).to receive(:add_observer).with(:observer) { |v| v } - expect(subject.add_observer(:observer)).to eq :observer - end - - it 'delegates #with_observer' do - expect(observer_set).to receive(:add_observer).with(:observer) { |v| v } - expect(subject.with_observer(:observer)).to eq subject - end - - it 'delegates #delete_observer' do - expect(observer_set).to receive(:delete_observer).with(:observer) - subject.delete_observer(:observer) - end - - it 'delegates #delete_observers' do - expect(observer_set).to receive(:delete_observers).with(no_args) - subject.delete_observers - end - - it 'delegates #count_observers' do - expect(observer_set).to receive(:count_observers).with(no_args) - subject.count_observers - end - end -end diff --git a/spec/concurrent/scheduled_task_spec.rb b/spec/concurrent/scheduled_task_spec.rb index 4fe489c95..4026e2e6e 100644 --- a/spec/concurrent/scheduled_task_spec.rb +++ b/spec/concurrent/scheduled_task_spec.rb @@ -1,7 +1,7 @@ require 'timecop' -require_relative 'dereferenceable_shared' -require_relative 'obligation_shared' -require_relative 'observable_shared' +require_relative 'concern/dereferenceable_shared' +require_relative 'concern/obligation_shared' +require_relative 'concern/observable_shared' module Concurrent diff --git a/spec/concurrent/timer_task_spec.rb b/spec/concurrent/timer_task_spec.rb index b138181b2..63df7ed73 100644 --- a/spec/concurrent/timer_task_spec.rb +++ b/spec/concurrent/timer_task_spec.rb @@ -1,5 +1,5 @@ -require_relative 'dereferenceable_shared' -require_relative 'observable_shared' +require_relative 'concern/dereferenceable_shared' +require_relative 'concern/observable_shared' module Concurrent From b7ec7db0a08bb8340fa6464d0383133ac1f23e39 Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Thu, 21 May 2015 16:28:39 -0600 Subject: [PATCH 03/11] Fixed SynchronizationObjectImpl::JavaObject --- ext/com/concurrent_ruby/ext/SynchronizationLibrary.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/com/concurrent_ruby/ext/SynchronizationLibrary.java b/ext/com/concurrent_ruby/ext/SynchronizationLibrary.java index 702a133ec..426ca1178 100644 --- a/ext/com/concurrent_ruby/ext/SynchronizationLibrary.java +++ b/ext/com/concurrent_ruby/ext/SynchronizationLibrary.java @@ -23,11 +23,11 @@ public class SynchronizationLibrary implements Library { public void load(Ruby runtime, boolean wrap) throws IOException { RubyModule synchronizationModule = runtime. defineModule("Concurrent"). - defineModuleUnder("Synchronization"); + defineModuleUnder("SynchronizationObjectImpl"); RubyClass parentClass = synchronizationModule.getClass("AbstractObject"); if (parentClass == null) - throw runtime.newRuntimeError("Concurrent::Synchronization::AbstractObject is missing"); + throw runtime.newRuntimeError("Concurrent::SynchronizationObjectImpl::AbstractObject is missing"); RubyClass synchronizedObjectJavaClass = synchronizationModule.defineClassUnder("JavaObject", parentClass, JRUBYREFERENCE_ALLOCATOR); From d8459820305ce0c297f584c4e77cfcc1d0ec63ef Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Thu, 21 May 2015 16:40:13 -0600 Subject: [PATCH 04/11] Moved all channel classes under the Channel module. --- .../channel/blocking_ring_buffer.rb | 112 +++++----- lib/concurrent/channel/buffered_channel.rb | 118 +++++------ lib/concurrent/channel/ring_buffer.rb | 112 +++++----- lib/concurrent/channel/unbuffered_channel.rb | 44 ++-- lib/concurrent/channel/waitable_list.rb | 48 ++--- .../channel/blocking_ring_buffer_spec.rb | 194 +++++++++--------- .../channel/buffered_channel_spec.rb | 188 ++++++++--------- spec/concurrent/channel/channel_spec.rb | 40 ++-- spec/concurrent/channel/probe_spec.rb | 92 +++++---- spec/concurrent/channel/ring_buffer_spec.rb | 168 +++++++-------- .../channel/unbuffered_channel_spec.rb | 156 +++++++------- 11 files changed, 647 insertions(+), 625 deletions(-) diff --git a/lib/concurrent/channel/blocking_ring_buffer.rb b/lib/concurrent/channel/blocking_ring_buffer.rb index 4d8650e5d..88891455c 100644 --- a/lib/concurrent/channel/blocking_ring_buffer.rb +++ b/lib/concurrent/channel/blocking_ring_buffer.rb @@ -1,76 +1,78 @@ require 'concurrent/synchronization_object' module Concurrent - class BlockingRingBuffer < SynchronizationObject + module Channel + class BlockingRingBuffer < SynchronizationObject - def initialize(capacity) - super(capacity) - end + def initialize(capacity) + super(capacity) + end - # @return [Integer] the capacity of the buffer - def capacity - synchronize { @buffer.capacity } - end + # @return [Integer] the capacity of the buffer + def capacity + synchronize { @buffer.capacity } + end - # @return [Integer] the number of elements currently in the buffer - def count - synchronize { @buffer.count } - end + # @return [Integer] the number of elements currently in the buffer + def count + synchronize { @buffer.count } + end - # @return [Boolean] true if buffer is empty, false otherwise - def empty? - synchronize { @buffer.empty? } - end + # @return [Boolean] true if buffer is empty, false otherwise + def empty? + synchronize { @buffer.empty? } + end - # @return [Boolean] true if buffer is full, false otherwise - def full? - synchronize { @buffer.full? } - end + # @return [Boolean] true if buffer is full, false otherwise + def full? + synchronize { @buffer.full? } + end - # @param [Object] value the value to be inserted - # @return [Boolean] true if value has been inserted, false otherwise - def put(value) - synchronize do - wait_while_full - @buffer.offer(value) - ns_signal - true + # @param [Object] value the value to be inserted + # @return [Boolean] true if value has been inserted, false otherwise + def put(value) + synchronize do + wait_while_full + @buffer.offer(value) + ns_signal + true + end end - end - # @return [Object] the first available value and removes it from the buffer. - # If buffer is empty it blocks until an element is available - def take - synchronize do - wait_while_empty - result = @buffer.poll - ns_signal - result + # @return [Object] the first available value and removes it from the buffer. + # If buffer is empty it blocks until an element is available + def take + synchronize do + wait_while_empty + result = @buffer.poll + ns_signal + result + end end - end - # @return [Object] the first available value and without removing it from - # the buffer. If buffer is empty returns nil - def peek - synchronize { @buffer.peek } - end + # @return [Object] the first available value and without removing it from + # the buffer. If buffer is empty returns nil + def peek + synchronize { @buffer.peek } + end - protected + protected - def ns_initialize(capacity) - @buffer = RingBuffer.new(capacity) - @first = @last = 0 - @count = 0 - end + def ns_initialize(capacity) + @buffer = RingBuffer.new(capacity) + @first = @last = 0 + @count = 0 + end - private + private - def wait_while_full - ns_wait_until { !@buffer.full? } - end + def wait_while_full + ns_wait_until { !@buffer.full? } + end - def wait_while_empty - ns_wait_until { !@buffer.empty? } + def wait_while_empty + ns_wait_until { !@buffer.empty? } + end end end end diff --git a/lib/concurrent/channel/buffered_channel.rb b/lib/concurrent/channel/buffered_channel.rb index cb6ed870a..7e4eaa9c2 100644 --- a/lib/concurrent/channel/buffered_channel.rb +++ b/lib/concurrent/channel/buffered_channel.rb @@ -1,83 +1,85 @@ require 'concurrent/channel/waitable_list' module Concurrent - class BufferedChannel + module Channel + class BufferedChannel - def initialize(size) - @mutex = Mutex.new - @condition = ConditionVariable.new - @buffer_condition = ConditionVariable.new + def initialize(size) + @mutex = Mutex.new + @condition = ConditionVariable.new + @buffer_condition = ConditionVariable.new - @probe_set = WaitableList.new - @buffer = RingBuffer.new(size) - end + @probe_set = WaitableList.new + @buffer = RingBuffer.new(size) + end - def probe_set_size - @probe_set.size - end + def probe_set_size + @probe_set.size + end - def buffer_queue_size - @mutex.synchronize { @buffer.count } - end + def buffer_queue_size + @mutex.synchronize { @buffer.count } + end - def push(value) - until set_probe_or_push_into_buffer(value) + def push(value) + until set_probe_or_push_into_buffer(value) + end end - end - def pop - probe = Channel::Probe.new - select(probe) - probe.value - end + def pop + probe = Channel::Probe.new + select(probe) + probe.value + end - def select(probe) - @mutex.synchronize do + def select(probe) + @mutex.synchronize do - if @buffer.empty? - @probe_set.put(probe) - true - else - shift_buffer if probe.try_set([peek_buffer, self]) - end + if @buffer.empty? + @probe_set.put(probe) + true + else + shift_buffer if probe.try_set([peek_buffer, self]) + end + end end - end - def remove_probe(probe) - @probe_set.delete(probe) - end + def remove_probe(probe) + @probe_set.delete(probe) + end - private + private - def push_into_buffer(value) - @buffer_condition.wait(@mutex) while @buffer.full? - @buffer.offer value - @buffer_condition.broadcast - end + def push_into_buffer(value) + @buffer_condition.wait(@mutex) while @buffer.full? + @buffer.offer value + @buffer_condition.broadcast + end - def peek_buffer - @buffer_condition.wait(@mutex) while @buffer.empty? - @buffer.peek - end + def peek_buffer + @buffer_condition.wait(@mutex) while @buffer.empty? + @buffer.peek + end - def shift_buffer - @buffer_condition.wait(@mutex) while @buffer.empty? - result = @buffer.poll - @buffer_condition.broadcast - result - end + def shift_buffer + @buffer_condition.wait(@mutex) while @buffer.empty? + result = @buffer.poll + @buffer_condition.broadcast + result + end - def set_probe_or_push_into_buffer(value) - @mutex.synchronize do - if @probe_set.empty? - push_into_buffer(value) - true - else - @probe_set.take.try_set([value, self]) + def set_probe_or_push_into_buffer(value) + @mutex.synchronize do + if @probe_set.empty? + push_into_buffer(value) + true + else + @probe_set.take.try_set([value, self]) + end end end - end + end end end diff --git a/lib/concurrent/channel/ring_buffer.rb b/lib/concurrent/channel/ring_buffer.rb index 1b19d5c5f..3e58a1a91 100644 --- a/lib/concurrent/channel/ring_buffer.rb +++ b/lib/concurrent/channel/ring_buffer.rb @@ -1,60 +1,62 @@ module Concurrent + module Channel + + # non-thread safe buffer + class RingBuffer + + def initialize(capacity) + @buffer = Array.new(capacity) + @first = @last = 0 + @count = 0 + end + + + # @return [Integer] the capacity of the buffer + def capacity + @buffer.size + end + + # @return [Integer] the number of elements currently in the buffer + def count + @count + end + + # @return [Boolean] true if buffer is empty, false otherwise + def empty? + @count == 0 + end + + # @return [Boolean] true if buffer is full, false otherwise + def full? + @count == capacity + end + + # @param [Object] value + # @return [Boolean] true if value has been inserted, false otherwise + def offer(value) + return false if full? + + @buffer[@last] = value + @last = (@last + 1) % @buffer.size + @count += 1 + true + end + + # @return [Object] the first available value and removes it from the buffer. If buffer is empty returns nil + def poll + result = @buffer[@first] + @buffer[@first] = nil + @first = (@first + 1) % @buffer.size + @count -= 1 + result + end + + # @return [Object] the first available value and without removing it from + # the buffer. If buffer is empty returns nil + def peek + @buffer[@first] + end - # non-thread safe buffer - class RingBuffer - - def initialize(capacity) - @buffer = Array.new(capacity) - @first = @last = 0 - @count = 0 - end - - - # @return [Integer] the capacity of the buffer - def capacity - @buffer.size - end - - # @return [Integer] the number of elements currently in the buffer - def count - @count - end - - # @return [Boolean] true if buffer is empty, false otherwise - def empty? - @count == 0 - end - - # @return [Boolean] true if buffer is full, false otherwise - def full? - @count == capacity end - - # @param [Object] value - # @return [Boolean] true if value has been inserted, false otherwise - def offer(value) - return false if full? - - @buffer[@last] = value - @last = (@last + 1) % @buffer.size - @count += 1 - true - end - - # @return [Object] the first available value and removes it from the buffer. If buffer is empty returns nil - def poll - result = @buffer[@first] - @buffer[@first] = nil - @first = (@first + 1) % @buffer.size - @count -= 1 - result - end - - # @return [Object] the first available value and without removing it from - # the buffer. If buffer is empty returns nil - def peek - @buffer[@first] - end - end end diff --git a/lib/concurrent/channel/unbuffered_channel.rb b/lib/concurrent/channel/unbuffered_channel.rb index c516fc8aa..20312feed 100644 --- a/lib/concurrent/channel/unbuffered_channel.rb +++ b/lib/concurrent/channel/unbuffered_channel.rb @@ -1,34 +1,36 @@ require_relative 'waitable_list' module Concurrent - class UnbufferedChannel + module Channel + class UnbufferedChannel - def initialize - @probe_set = WaitableList.new - end + def initialize + @probe_set = WaitableList.new + end - def probe_set_size - @probe_set.size - end + def probe_set_size + @probe_set.size + end - def push(value) - until @probe_set.take.try_set([value, self]) + def push(value) + until @probe_set.take.try_set([value, self]) + end end - end - def pop - probe = Channel::Probe.new - select(probe) - probe.value - end + def pop + probe = Channel::Probe.new + select(probe) + probe.value + end - def select(probe) - @probe_set.put(probe) - end + def select(probe) + @probe_set.put(probe) + end - def remove_probe(probe) - @probe_set.delete(probe) - end + def remove_probe(probe) + @probe_set.delete(probe) + end + end end end diff --git a/lib/concurrent/channel/waitable_list.rb b/lib/concurrent/channel/waitable_list.rb index 415fbbb8b..d5c5106fe 100644 --- a/lib/concurrent/channel/waitable_list.rb +++ b/lib/concurrent/channel/waitable_list.rb @@ -1,38 +1,40 @@ require 'concurrent/synchronization_object' module Concurrent - class WaitableList < SynchronizationObject + module Channel + class WaitableList < SynchronizationObject - def size - synchronize { @list.size } - end + def size + synchronize { @list.size } + end - def empty? - synchronize { @list.empty? } - end + def empty? + synchronize { @list.empty? } + end - def put(value) - synchronize do - @list << value - ns_signal + def put(value) + synchronize do + @list << value + ns_signal + end end - end - def delete(value) - synchronize { @list.delete(value) } - end + def delete(value) + synchronize { @list.delete(value) } + end - def take - synchronize do - ns_wait_until { !@list.empty? } - @list.shift + def take + synchronize do + ns_wait_until { !@list.empty? } + @list.shift + end end - end - protected + protected - def ns_initialize - @list = [] + def ns_initialize + @list = [] + end end end end diff --git a/spec/concurrent/channel/blocking_ring_buffer_spec.rb b/spec/concurrent/channel/blocking_ring_buffer_spec.rb index a35ce469a..14ced96f8 100644 --- a/spec/concurrent/channel/blocking_ring_buffer_spec.rb +++ b/spec/concurrent/channel/blocking_ring_buffer_spec.rb @@ -1,147 +1,149 @@ module Concurrent + module Channel - describe BlockingRingBuffer do + describe BlockingRingBuffer do - let(:capacity) { 3 } - let(:buffer) { BlockingRingBuffer.new(capacity) } + let(:capacity) { 3 } + let(:buffer) { BlockingRingBuffer.new(capacity) } - def fill_buffer - capacity.times { buffer.put 3 } - end - - describe '#capacity' do - it 'returns the value passed in constructor' do - expect(buffer.capacity).to eq capacity + def fill_buffer + capacity.times { buffer.put 3 } end - end - describe '#count' do - it 'is zero when created' do - expect(buffer.count).to eq 0 + describe '#capacity' do + it 'returns the value passed in constructor' do + expect(buffer.capacity).to eq capacity + end end - it 'increases when an element is added' do - buffer.put 5 - expect(buffer.count).to eq 1 + describe '#count' do + it 'is zero when created' do + expect(buffer.count).to eq 0 + end - buffer.put 1 - expect(buffer.count).to eq 2 - end + it 'increases when an element is added' do + buffer.put 5 + expect(buffer.count).to eq 1 - it 'decreases when an element is removed' do - buffer.put 10 + buffer.put 1 + expect(buffer.count).to eq 2 + end - buffer.take + it 'decreases when an element is removed' do + buffer.put 10 - expect(buffer.count).to eq 0 - end - end + buffer.take - describe '#empty?' do - it 'is true when count is zero' do - expect(buffer.empty?).to be_truthy + expect(buffer.count).to eq 0 + end end - it 'is false when count is not zero' do - buffer.put 82 - expect(buffer.empty?).to be_falsey - end - end + describe '#empty?' do + it 'is true when count is zero' do + expect(buffer.empty?).to be_truthy + end - describe '#full?' do - it 'is true when count is capacity' do - fill_buffer - expect(buffer.full?).to be_truthy + it 'is false when count is not zero' do + buffer.put 82 + expect(buffer.empty?).to be_falsey + end end - it 'is false when count is not capacity' do - expect(buffer.full?).to be_falsey + describe '#full?' do + it 'is true when count is capacity' do + fill_buffer + expect(buffer.full?).to be_truthy + end + + it 'is false when count is not capacity' do + expect(buffer.full?).to be_falsey + end end - end - describe '#put' do - it 'block when buffer is full' do - fill_buffer + describe '#put' do + it 'block when buffer is full' do + fill_buffer - t = Thread.new { buffer.put 32 } + t = Thread.new { buffer.put 32 } - sleep(0.1) + sleep(0.1) - expect(t.status).to eq 'sleep' - end + expect(t.status).to eq 'sleep' + end - it 'continues when an element is removed' do - latch = CountDownLatch.new(1) + it 'continues when an element is removed' do + latch = CountDownLatch.new(1) - Thread.new { (capacity + 1).times { buffer.put 'hi' }; latch.count_down } - Thread.new { sleep(0.1); buffer.take } + Thread.new { (capacity + 1).times { buffer.put 'hi' }; latch.count_down } + Thread.new { sleep(0.1); buffer.take } - expect(latch.wait(0.2)).to be_truthy + expect(latch.wait(0.2)).to be_truthy + end end - end - describe '#take' do - it 'blocks when buffer is empty' do - t = Thread.new { buffer.take } + describe '#take' do + it 'blocks when buffer is empty' do + t = Thread.new { buffer.take } - sleep(0.1) + sleep(0.1) - expect(t.status).to eq 'sleep' - end + expect(t.status).to eq 'sleep' + end - it 'continues when an element is added' do - latch = CountDownLatch.new(1) + it 'continues when an element is added' do + latch = CountDownLatch.new(1) - Thread.new { buffer.take; latch.count_down } - Thread.new { sleep(0.1); buffer.put 3 } + Thread.new { buffer.take; latch.count_down } + Thread.new { sleep(0.1); buffer.put 3 } - expect(latch.wait(0.2)).to be_truthy - end + expect(latch.wait(0.2)).to be_truthy + end - it 'returns the first added value' do - buffer.put 'hi' - buffer.put 'foo' - buffer.put 'bar' + it 'returns the first added value' do + buffer.put 'hi' + buffer.put 'foo' + buffer.put 'bar' - expect(buffer.take).to eq 'hi' - expect(buffer.take).to eq 'foo' - expect(buffer.take).to eq 'bar' + expect(buffer.take).to eq 'hi' + expect(buffer.take).to eq 'foo' + expect(buffer.take).to eq 'bar' + end end - end - describe '#peek' do - context 'buffer empty' do - it 'returns nil when buffer is empty' do - expect(buffer.peek).to be_nil + describe '#peek' do + context 'buffer empty' do + it 'returns nil when buffer is empty' do + expect(buffer.peek).to be_nil + end end - end - context 'not empty' do + context 'not empty' do - before(:each) { buffer.put 'element' } + before(:each) { buffer.put 'element' } - it 'returns the first value' do - expect(buffer.peek).to eq 'element' - end + it 'returns the first value' do + expect(buffer.peek).to eq 'element' + end - it 'does not change buffer' do - buffer.peek - expect(buffer.count).to eq 1 + it 'does not change buffer' do + buffer.peek + expect(buffer.count).to eq 1 + end end end - end - context 'circular condition' do - it 'can filled many times' do - fill_buffer - capacity.times { buffer.take } + context 'circular condition' do + it 'can filled many times' do + fill_buffer + capacity.times { buffer.take } - buffer.put 'hi' + buffer.put 'hi' - expect(buffer.take).to eq 'hi' - expect(buffer.capacity).to eq capacity + expect(buffer.take).to eq 'hi' + expect(buffer.capacity).to eq capacity + end end - end + end end end diff --git a/spec/concurrent/channel/buffered_channel_spec.rb b/spec/concurrent/channel/buffered_channel_spec.rb index f4b4b4640..2db5fdb3f 100644 --- a/spec/concurrent/channel/buffered_channel_spec.rb +++ b/spec/concurrent/channel/buffered_channel_spec.rb @@ -1,149 +1,151 @@ module Concurrent + module Channel - describe BufferedChannel do + describe BufferedChannel do - let(:size) { 2 } - let!(:channel) { BufferedChannel.new(size) } - let(:probe) { Channel::Probe.new } + let(:size) { 2 } + let!(:channel) { BufferedChannel.new(size) } + let(:probe) { Channel::Probe.new } - context 'without timeout' do + context 'without timeout' do - describe '#push' do - it 'adds elements to buffer' do - expect(channel.buffer_queue_size).to be 0 + describe '#push' do + it 'adds elements to buffer' do + expect(channel.buffer_queue_size).to be 0 - channel.push('a') - channel.push('a') + channel.push('a') + channel.push('a') - expect(channel.buffer_queue_size).to be 2 - end + expect(channel.buffer_queue_size).to be 2 + end - it 'should block when buffer is full' do - channel.push 1 - channel.push 2 + it 'should block when buffer is full' do + channel.push 1 + channel.push 2 - t = Thread.new { channel.push 3 } - sleep(0.05) - expect(t.status).to eq 'sleep' - end + t = Thread.new { channel.push 3 } + sleep(0.05) + expect(t.status).to eq 'sleep' + end - it 'restarts thread when buffer is no more full' do - channel.push 'hi' - channel.push 'foo' + it 'restarts thread when buffer is no more full' do + channel.push 'hi' + channel.push 'foo' - result = nil + result = nil - Thread.new { channel.push 'bar'; result = 42 } + Thread.new { channel.push 'bar'; result = 42 } - sleep(0.1) + sleep(0.1) - channel.pop + channel.pop - sleep(0.1) + sleep(0.1) - expect(result).to eq 42 - end + expect(result).to eq 42 + end - it 'should assign value to a probe if probe set is not empty' do - channel.select(probe) - Thread.new { sleep(0.1); channel.push 3 } - expect(probe.value.first).to eq 3 + it 'should assign value to a probe if probe set is not empty' do + channel.select(probe) + Thread.new { sleep(0.1); channel.push 3 } + expect(probe.value.first).to eq 3 + end end - end - describe '#pop' do - it 'should block if buffer is empty' do - t = Thread.new { channel.pop } - sleep(0.05) - expect(t.status).to eq 'sleep' - end + describe '#pop' do + it 'should block if buffer is empty' do + t = Thread.new { channel.pop } + sleep(0.05) + expect(t.status).to eq 'sleep' + end - it 'returns value if buffer is not empty' do - channel.push 1 - result = channel.pop + it 'returns value if buffer is not empty' do + channel.push 1 + result = channel.pop - expect(result.first).to eq 1 - end + expect(result.first).to eq 1 + end - it 'removes the first value from the buffer' do - channel.push 'a' - channel.push 'b' + it 'removes the first value from the buffer' do + channel.push 'a' + channel.push 'b' - expect(channel.pop.first).to eq 'a' - expect(channel.buffer_queue_size).to eq 1 + expect(channel.pop.first).to eq 'a' + expect(channel.buffer_queue_size).to eq 1 + end end + end - end + describe 'select' do - describe 'select' do + it 'does not block' do + t = Thread.new { channel.select(probe) } - it 'does not block' do - t = Thread.new { channel.select(probe) } + sleep(0.05) - sleep(0.05) + expect(t.status).to eq false + end - expect(t.status).to eq false - end + it 'gets notified by writer thread' do + channel.select(probe) - it 'gets notified by writer thread' do - channel.select(probe) + Thread.new { channel.push 82 } - Thread.new { channel.push 82 } + expect(probe.value.first).to eq 82 + end - expect(probe.value.first).to eq 82 end - end - - context 'already set probes' do - context 'empty buffer' do - it 'discards already set probes' do - probe.set('set value') + context 'already set probes' do + context 'empty buffer' do + it 'discards already set probes' do + probe.set('set value') - channel.select(probe) + channel.select(probe) - channel.push 27 + channel.push 27 - expect(channel.buffer_queue_size).to eq 1 - expect(channel.probe_set_size).to eq 0 + expect(channel.buffer_queue_size).to eq 1 + expect(channel.probe_set_size).to eq 0 + end end - end - context 'empty probe set' do - it 'discards set probe' do - probe.set('set value') + context 'empty probe set' do + it 'discards set probe' do + probe.set('set value') - channel.push 82 + channel.push 82 - channel.select(probe) + channel.select(probe) - expect(channel.buffer_queue_size).to eq 1 + expect(channel.buffer_queue_size).to eq 1 - expect(channel.pop.first).to eq 82 + expect(channel.pop.first).to eq 82 + end end end - end - describe 'probe set' do + describe 'probe set' do - it 'has size zero after creation' do - expect(channel.probe_set_size).to eq 0 - end + it 'has size zero after creation' do + expect(channel.probe_set_size).to eq 0 + end - it 'increases size after a select' do - channel.select(probe) - expect(channel.probe_set_size).to eq 1 - end + it 'increases size after a select' do + channel.select(probe) + expect(channel.probe_set_size).to eq 1 + end + + it 'decreases size after a removal' do + channel.select(probe) + channel.remove_probe(probe) + expect(channel.probe_set_size).to eq 0 + end - it 'decreases size after a removal' do - channel.select(probe) - channel.remove_probe(probe) - expect(channel.probe_set_size).to eq 0 end end - end end diff --git a/spec/concurrent/channel/channel_spec.rb b/spec/concurrent/channel/channel_spec.rb index cbefbb2d3..b9504192b 100644 --- a/spec/concurrent/channel/channel_spec.rb +++ b/spec/concurrent/channel/channel_spec.rb @@ -1,37 +1,39 @@ module Concurrent + module Channel - describe Channel do + describe Channel do - describe '.select' do + describe '.select' do - context 'without timeout' do - it 'returns the first value available on a channel' do - channels = [ UnbufferedChannel.new, UnbufferedChannel.new] + context 'without timeout' do + it 'returns the first value available on a channel' do + channels = [ UnbufferedChannel.new, UnbufferedChannel.new] - Thread.new { channels[1].push 77 } + Thread.new { channels[1].push 77 } - value, channel = Channel.select(*channels) + value, channel = Channel.select(*channels) - expect(value).to eq 77 - expect(channel).to be channels[1] - end + expect(value).to eq 77 + expect(channel).to be channels[1] + end - it 'cleans up' do - channels = [ UnbufferedChannel.new, UnbufferedChannel.new] - channels.each { |ch| allow(ch).to receive(:remove_probe).with( an_instance_of(Channel::Probe) )} + it 'cleans up' do + channels = [ UnbufferedChannel.new, UnbufferedChannel.new] + channels.each { |ch| allow(ch).to receive(:remove_probe).with( an_instance_of(Channel::Probe) )} - Thread.new { channels[1].push 77 } + Thread.new { channels[1].push 77 } - value, channel = Channel.select(*channels) + value, channel = Channel.select(*channels) - expect(value).to eq 77 - expect(channel).to be channels[1] + expect(value).to eq 77 + expect(channel).to be channels[1] - channels.each { |ch| expect(ch).to have_received(:remove_probe).with( an_instance_of(Channel::Probe) ) } + channels.each { |ch| expect(ch).to have_received(:remove_probe).with( an_instance_of(Channel::Probe) ) } + end end + end end - end end diff --git a/spec/concurrent/channel/probe_spec.rb b/spec/concurrent/channel/probe_spec.rb index 817a3053f..e165680a9 100644 --- a/spec/concurrent/channel/probe_spec.rb +++ b/spec/concurrent/channel/probe_spec.rb @@ -1,69 +1,71 @@ require_relative '../concern/observable_shared' module Concurrent + module Channel - describe Channel::Probe do + describe Channel::Probe do - let(:channel) { Object.new } - let(:probe) { Channel::Probe.new } + let(:channel) { Object.new } + let(:probe) { Channel::Probe.new } - describe 'behavior' do + describe 'behavior' do - # observable + # observable - subject{ Channel::Probe.new } + subject{ Channel::Probe.new } - def trigger_observable(observable) - observable.set('value') - end - - it_should_behave_like :observable - end - - describe '#try_set' do - context 'empty probe' do - it 'assigns the value' do - probe.try_set([32, channel]) - expect(probe.value.first).to eq 32 + def trigger_observable(observable) + observable.set('value') end - it 'assign the channel' do - probe.try_set([32, channel]) - expect(probe.value.last).to be channel - end + it_should_behave_like :observable + end - it 'returns true' do - expect(probe.try_set(['hi', channel])).to eq true + describe '#try_set' do + context 'empty probe' do + it 'assigns the value' do + probe.try_set([32, channel]) + expect(probe.value.first).to eq 32 + end + + it 'assign the channel' do + probe.try_set([32, channel]) + expect(probe.value.last).to be channel + end + + it 'returns true' do + expect(probe.try_set(['hi', channel])).to eq true + end end - end - context 'fulfilled probe' do - before(:each) { probe.set([27, nil]) } + context 'fulfilled probe' do + before(:each) { probe.set([27, nil]) } - it 'does not assign the value' do - probe.try_set([88, channel]) - expect(probe.value.first).to eq 27 - end + it 'does not assign the value' do + probe.try_set([88, channel]) + expect(probe.value.first).to eq 27 + end - it 'returns false' do - expect(probe.try_set(['hello', channel])).to eq false + it 'returns false' do + expect(probe.try_set(['hello', channel])).to eq false + end end - end - context 'rejected probe' do - before(:each) { probe.fail } + context 'rejected probe' do + before(:each) { probe.fail } - it 'does not assign the value' do - probe.try_set([88, channel]) - expect(probe).to be_rejected - end + it 'does not assign the value' do + probe.try_set([88, channel]) + expect(probe).to be_rejected + end - it 'has a nil value' do - expect(probe.value).to be_nil - end + it 'has a nil value' do + expect(probe.value).to be_nil + end - it 'returns false' do - expect(probe.try_set(['hello', channel])).to eq false + it 'returns false' do + expect(probe.try_set(['hello', channel])).to eq false + end end end end diff --git a/spec/concurrent/channel/ring_buffer_spec.rb b/spec/concurrent/channel/ring_buffer_spec.rb index 07f03babe..d49f4bc8b 100644 --- a/spec/concurrent/channel/ring_buffer_spec.rb +++ b/spec/concurrent/channel/ring_buffer_spec.rb @@ -1,124 +1,126 @@ module Concurrent + module Channel - describe RingBuffer do + describe RingBuffer do - let(:capacity) { 3 } - let(:buffer) { RingBuffer.new(capacity) } + let(:capacity) { 3 } + let(:buffer) { RingBuffer.new(capacity) } - def fill_buffer - capacity.times { buffer.offer 3 } - end - - describe '#capacity' do - it 'returns the value passed in constructor' do - expect(buffer.capacity).to eq capacity + def fill_buffer + capacity.times { buffer.offer 3 } end - end - describe '#count' do - it 'is zero when created' do - expect(buffer.count).to eq 0 + describe '#capacity' do + it 'returns the value passed in constructor' do + expect(buffer.capacity).to eq capacity + end end - it 'increases when an element is added' do - buffer.offer 5 - expect(buffer.count).to eq 1 - - buffer.offer 1 - expect(buffer.count).to eq 2 - end + describe '#count' do + it 'is zero when created' do + expect(buffer.count).to eq 0 + end - it 'decreases when an element is removed' do - buffer.offer 10 - buffer.poll + it 'increases when an element is added' do + buffer.offer 5 + expect(buffer.count).to eq 1 - expect(buffer.count).to eq 0 - end - end + buffer.offer 1 + expect(buffer.count).to eq 2 + end - describe '#empty?' do - it 'is true when count is zero' do - expect(buffer.empty?).to be_truthy - end + it 'decreases when an element is removed' do + buffer.offer 10 + buffer.poll - it 'is false when count is not zero' do - buffer.offer 82 - expect(buffer.empty?).to be_falsey + expect(buffer.count).to eq 0 + end end - end - describe '#full?' do - it 'is true when count is capacity' do - fill_buffer - expect(buffer.full?).to be_truthy - end + describe '#empty?' do + it 'is true when count is zero' do + expect(buffer.empty?).to be_truthy + end - it 'is false when count is not capacity' do - expect(buffer.full?).to be_falsey + it 'is false when count is not zero' do + buffer.offer 82 + expect(buffer.empty?).to be_falsey + end end - end - describe '#offer' do - it 'returns false when buffer is full' do - fill_buffer - expect(buffer.offer(3)).to be_falsey - end + describe '#full?' do + it 'is true when count is capacity' do + fill_buffer + expect(buffer.full?).to be_truthy + end - it 'returns true when the buffer is not full' do - expect(buffer.offer(5)).to be_truthy + it 'is false when count is not capacity' do + expect(buffer.full?).to be_falsey + end end - end + describe '#offer' do + it 'returns false when buffer is full' do + fill_buffer + expect(buffer.offer(3)).to be_falsey + end - describe '#poll' do - it 'returns the first added value' do - buffer.offer 'hi' - buffer.offer 'foo' - buffer.offer 'bar' + it 'returns true when the buffer is not full' do + expect(buffer.offer(5)).to be_truthy + end - expect(buffer.poll).to eq 'hi' - expect(buffer.poll).to eq 'foo' - expect(buffer.poll).to eq 'bar' end - it 'returns nil when buffer is empty' do - expect(buffer.poll).to be_nil - end - end + describe '#poll' do + it 'returns the first added value' do + buffer.offer 'hi' + buffer.offer 'foo' + buffer.offer 'bar' + + expect(buffer.poll).to eq 'hi' + expect(buffer.poll).to eq 'foo' + expect(buffer.poll).to eq 'bar' + end - describe '#peek' do - context 'buffer empty' do it 'returns nil when buffer is empty' do - expect(buffer.peek).to be_nil + expect(buffer.poll).to be_nil end end - context 'not empty' do + describe '#peek' do + context 'buffer empty' do + it 'returns nil when buffer is empty' do + expect(buffer.peek).to be_nil + end + end - before(:each) { buffer.offer 'element' } + context 'not empty' do - it 'returns the first value' do - expect(buffer.peek).to eq 'element' - end + before(:each) { buffer.offer 'element' } - it 'does not change buffer' do - buffer.peek - expect(buffer.count).to eq 1 + it 'returns the first value' do + expect(buffer.peek).to eq 'element' + end + + it 'does not change buffer' do + buffer.peek + expect(buffer.count).to eq 1 + end end end - end - context 'circular condition' do - it 'can filled many times' do - fill_buffer - capacity.times { buffer.poll } + context 'circular condition' do + it 'can filled many times' do + fill_buffer + capacity.times { buffer.poll } - buffer.offer 'hi' + buffer.offer 'hi' - expect(buffer.poll).to eq 'hi' - expect(buffer.capacity).to eq capacity + expect(buffer.poll).to eq 'hi' + expect(buffer.capacity).to eq capacity + end end - end + end end end diff --git a/spec/concurrent/channel/unbuffered_channel_spec.rb b/spec/concurrent/channel/unbuffered_channel_spec.rb index a2ededc71..6ed5ee760 100644 --- a/spec/concurrent/channel/unbuffered_channel_spec.rb +++ b/spec/concurrent/channel/unbuffered_channel_spec.rb @@ -1,130 +1,132 @@ module Concurrent + module Channel - describe UnbufferedChannel do + describe UnbufferedChannel do - let!(:channel) { subject } - let(:probe) { Channel::Probe.new } + let!(:channel) { subject } + let(:probe) { Channel::Probe.new } - context 'with one thread' do + context 'with one thread' do - context 'without timeout' do + context 'without timeout' do - describe '#push' do - it 'should block' do - t = Thread.new { channel.push 5 } - sleep(0.05) - expect(t.status).to eq 'sleep' + describe '#push' do + it 'should block' do + t = Thread.new { channel.push 5 } + sleep(0.05) + expect(t.status).to eq 'sleep' + end end - end - describe '#pop' do - it 'should block' do - t = Thread.new { channel.pop } - sleep(0.05) - expect(t.status).to eq 'sleep' + describe '#pop' do + it 'should block' do + t = Thread.new { channel.pop } + sleep(0.05) + expect(t.status).to eq 'sleep' + end end + end end - end - - context 'cooperating threads' do + context 'cooperating threads' do - it 'passes the pushed value to thread waiting on pop' do - result = nil + it 'passes the pushed value to thread waiting on pop' do + result = nil - Thread.new { channel.push 42 } - Thread.new { result = channel.pop; } + Thread.new { channel.push 42 } + Thread.new { result = channel.pop; } - sleep(0.1) + sleep(0.1) - expect(result.first).to eq 42 - end + expect(result.first).to eq 42 + end - it 'passes the pushed value to only one thread' do - result = [] + it 'passes the pushed value to only one thread' do + result = [] - Thread.new { channel.push 37 } - Thread.new { result << channel.pop } - Thread.new { result << channel.pop } + Thread.new { channel.push 37 } + Thread.new { result << channel.pop } + Thread.new { result << channel.pop } - sleep(0.1) + sleep(0.1) - expect(result.size).to eq(1) - end + expect(result.size).to eq(1) + end - it 'gets the pushed value when ready' do - result = nil + it 'gets the pushed value when ready' do + result = nil - Thread.new { result = channel.pop; } - Thread.new { channel.push 57 } + Thread.new { result = channel.pop; } + Thread.new { channel.push 57 } - sleep(0.1) + sleep(0.1) - expect(result.first).to eq 57 + expect(result.first).to eq 57 + end end - end - describe 'select' do + describe 'select' do - it 'does not block' do - t = Thread.new { channel.select(probe) } + it 'does not block' do + t = Thread.new { channel.select(probe) } - sleep(0.05) + sleep(0.05) - expect(t.status).to eq false - end + expect(t.status).to eq false + end - it 'gets notified by writer thread' do - channel.select(probe) + it 'gets notified by writer thread' do + channel.select(probe) - Thread.new { channel.push 82 } + Thread.new { channel.push 82 } - expect(probe.value.first).to eq 82 - end + expect(probe.value.first).to eq 82 + end + + it 'ignores already set probes and waits for a new one' do + probe.set(27) - it 'ignores already set probes and waits for a new one' do - probe.set(27) + channel.select(probe) - channel.select(probe) + t = Thread.new { channel.push 72 } - t = Thread.new { channel.push 72 } + sleep(0.05) - sleep(0.05) + expect(t.status).to eq 'sleep' - expect(t.status).to eq 'sleep' + new_probe = Channel::Probe.new - new_probe = Channel::Probe.new + channel.select(new_probe) - channel.select(new_probe) + sleep(0.05) - sleep(0.05) + expect(new_probe.value.first).to eq 72 + end - expect(new_probe.value.first).to eq 72 end - end + describe 'probe set' do - describe 'probe set' do + it 'has size zero after creation' do + expect(channel.probe_set_size).to eq 0 + end - it 'has size zero after creation' do - expect(channel.probe_set_size).to eq 0 - end + it 'increases size after a select' do + channel.select(probe) + expect(channel.probe_set_size).to eq 1 + end - it 'increases size after a select' do - channel.select(probe) - expect(channel.probe_set_size).to eq 1 - end + it 'decreases size after a removal' do + channel.select(probe) + channel.remove_probe(probe) + expect(channel.probe_set_size).to eq 0 + end - it 'decreases size after a removal' do - channel.select(probe) - channel.remove_probe(probe) - expect(channel.probe_set_size).to eq 0 end - end - + end end end From 8f423fe5a7e3559823f0aa76f87d666245afdadc Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Thu, 21 May 2015 17:00:36 -0600 Subject: [PATCH 05/11] Moved PriorityQueue to the Concurrent::Collection module. --- lib/concurrent/collection/priority_queue.rb | 354 ++---------------- .../java_priority_queue.rb | 80 ++++ .../mutex_priority_queue.rb | 225 +++++++++++ lib/concurrent/executor/timer_set.rb | 2 +- .../collection/priority_queue_spec.rb | 34 +- 5 files changed, 358 insertions(+), 337 deletions(-) create mode 100644 lib/concurrent/collection/priority_queue_impl/java_priority_queue.rb create mode 100644 lib/concurrent/collection/priority_queue_impl/mutex_priority_queue.rb diff --git a/lib/concurrent/collection/priority_queue.rb b/lib/concurrent/collection/priority_queue.rb index 1e49b14b7..d609a1688 100644 --- a/lib/concurrent/collection/priority_queue.rb +++ b/lib/concurrent/collection/priority_queue.rb @@ -1,348 +1,60 @@ -module Concurrent - - # @!macro [attach] priority_queue - # - # A queue collection in which the elements are sorted based on their - # comparison (spaceship) operator `<=>`. Items are added to the queue - # at a position relative to their priority. On removal the element - # with the "highest" priority is removed. By default the sort order is - # from highest to lowest, but a lowest-to-highest sort order can be - # set on construction. - # - # The API is based on the `Queue` class from the Ruby standard library. - # - # The pure Ruby implementation, `MutexPriorityQueue` uses a heap algorithm - # stored in an array. The algorithm is based on the work of Robert Sedgewick - # and Kevin Wayne. - # - # The JRuby native implementation is a thin wrapper around the standard - # library `java.util.PriorityQueue`. - # - # When running under JRuby the class `PriorityQueue` extends `JavaPriorityQueue`. - # When running under all other interpreters it extends `MutexPriorityQueue`. - # - # @note This implementation is *not* thread safe. - # - # @see http://en.wikipedia.org/wiki/Priority_queue - # @see http://ruby-doc.org/stdlib-2.0.0/libdoc/thread/rdoc/Queue.html - # - # @see http://algs4.cs.princeton.edu/24pq/index.php#2.6 - # @see http://algs4.cs.princeton.edu/24pq/MaxPQ.java.html - # - # @see http://docs.oracle.com/javase/7/docs/api/java/util/PriorityQueue.html - class MutexPriorityQueue - - # @!macro [attach] priority_queue_method_initialize - # - # Create a new priority queue with no items. - # - # @param [Hash] opts the options for creating the queue - # @option opts [Symbol] :order (:max) dictates the order in which items are - # stored: from highest to lowest when `:max` or `:high`; from lowest to - # highest when `:min` or `:low` - def initialize(opts = {}) - order = opts.fetch(:order, :max) - @comparator = [:min, :low].include?(order) ? -1 : 1 - clear - end - - # @!macro [attach] priority_queue_method_clear - # - # Removes all of the elements from this priority queue. - def clear - @queue = [nil] - @length = 0 - true - end - - # @!macro [attach] priority_queue_method_delete - # - # Deletes all items from `self` that are equal to `item`. - # - # @param [Object] item the item to be removed from the queue - # @return [Object] true if the item is found else false - def delete(item) - original_length = @length - k = 1 - while k <= @length - if @queue[k] == item - swap(k, @length) - @length -= 1 - sink(k) - @queue.pop - else - k += 1 - end - end - @length != original_length - end - - # @!macro [attach] priority_queue_method_empty - # - # Returns `true` if `self` contains no elements. - # - # @return [Boolean] true if there are no items in the queue else false - def empty? - size == 0 - end - - # @!macro [attach] priority_queue_method_include - # - # Returns `true` if the given item is present in `self` (that is, if any - # element == `item`), otherwise returns false. - # - # @param [Object] item the item to search for - # - # @return [Boolean] true if the item is found else false - def include?(item) - @queue.include?(item) - end - alias_method :has_priority?, :include? - - # @!macro [attach] priority_queue_method_length - # - # The current length of the queue. - # - # @return [Fixnum] the number of items in the queue - def length - @length - end - alias_method :size, :length - - # @!macro [attach] priority_queue_method_peek - # - # Retrieves, but does not remove, the head of this queue, or returns `nil` - # if this queue is empty. - # - # @return [Object] the head of the queue or `nil` when empty - def peek - @queue[1] - end - - # @!macro [attach] priority_queue_method_pop - # - # Retrieves and removes the head of this queue, or returns `nil` if this - # queue is empty. - # - # @return [Object] the head of the queue or `nil` when empty - def pop - max = @queue[1] - swap(1, @length) - @length -= 1 - sink(1) - @queue.pop - max - end - alias_method :deq, :pop - alias_method :shift, :pop - - # @!macro [attach] priority_queue_method_push - # - # Inserts the specified element into this priority queue. - # - # @param [Object] item the item to insert onto the queue - def push(item) - @length += 1 - @queue << item - swim(@length) - true - end - alias_method :<<, :push - alias_method :enq, :push - - # @!macro [attach] priority_queue_method_from_list - # - # Create a new priority queue from the given list. - # - # @param [Enumerable] list the list to build the queue from - # @param [Hash] opts the options for creating the queue - # - # @return [PriorityQueue] the newly created and populated queue - def self.from_list(list, opts = {}) - queue = new(opts) - list.each{|item| queue << item } - queue - end - - protected +require 'concurrent/collection/priority_queue_impl/java_priority_queue' +require 'concurrent/collection/priority_queue_impl/mutex_priority_queue' - # Exchange the values at the given indexes within the internal array. - # - # @param [Integer] x the first index to swap - # @param [Integer] y the second index to swap - # - # @!visibility private - def swap(x, y) - temp = @queue[x] - @queue[x] = @queue[y] - @queue[y] = temp - end - - # Are the items at the given indexes ordered based on the priority - # order specified at construction? - # - # @param [Integer] x the first index from which to retrieve a comparable value - # @param [Integer] y the second index from which to retrieve a comparable value - # - # @return [Boolean] true if the two elements are in the correct priority order - # else false - # - # @!visibility private - def ordered?(x, y) - (@queue[x] <=> @queue[y]) == @comparator - end - - # Percolate down to maintain heap invariant. - # - # @param [Integer] k the index at which to start the percolation - # - # @!visibility private - def sink(k) - while (j = (2 * k)) <= @length do - j += 1 if j < @length && ! ordered?(j, j+1) - break if ordered?(k, j) - swap(k, j) - k = j - end - end +module Concurrent + module Collection - # Percolate up to maintain heap invariant. - # - # @param [Integer] k the index at which to start the percolation - # - # @!visibility private - def swim(k) - while k > 1 && ! ordered?(k/2, k) do - swap(k, k/2) - k = k/2 - end + module PriorityQueueImpl + Implementation = case + when Concurrent.on_jruby? + JavaPriorityQueue + else + MutexPriorityQueue + end end - end - - if Concurrent.on_jruby? # @!macro priority_queue - class JavaPriorityQueue - - # @!macro priority_queue_method_initialize - def initialize(opts = {}) - order = opts.fetch(:order, :max) - if [:min, :low].include?(order) - @queue = java.util.PriorityQueue.new(11) # 11 is the default initial capacity - else - @queue = java.util.PriorityQueue.new(11, java.util.Collections.reverseOrder()) - end - end - - # @!macro priority_queue_method_clear - def clear - @queue.clear - true - end - - # @!macro priority_queue_method_delete - def delete(item) - found = false - while @queue.remove(item) do - found = true - end - found - end - - # @!macro priority_queue_method_empty - def empty? - @queue.size == 0 - end + class PriorityQueue < PriorityQueueImpl::Implementation - # @!macro priority_queue_method_include - def include?(item) - @queue.contains(item) - end alias_method :has_priority?, :include? - # @!macro priority_queue_method_length - def length - @queue.size - end alias_method :size, :length - # @!macro priority_queue_method_peek - def peek - @queue.peek - end - - # @!macro priority_queue_method_pop - def pop - @queue.poll - end alias_method :deq, :pop alias_method :shift, :pop - # @!macro priority_queue_method_push - def push(item) - @queue.add(item) - end alias_method :<<, :push alias_method :enq, :push - # @!macro priority_queue_method_from_list - def self.from_list(list, opts = {}) - queue = new(opts) - list.each{|item| queue << item } - queue - end - end - end - - PriorityQueueImplementation = case - when Concurrent.on_jruby? - JavaPriorityQueue - else - MutexPriorityQueue - end - private_constant :PriorityQueueImplementation - - # @!macro priority_queue - class PriorityQueue < PriorityQueueImplementation - - alias_method :has_priority?, :include? + # @!method initialize(opts = {}) + # @!macro priority_queue_method_initialize - alias_method :size, :length + # @!method clear + # @!macro priority_queue_method_clear - alias_method :deq, :pop - alias_method :shift, :pop + # @!method delete(item) + # @!macro priority_queue_method_delete - alias_method :<<, :push - alias_method :enq, :push + # @!method empty? + # @!macro priority_queue_method_empty - # @!method initialize(opts = {}) - # @!macro priority_queue_method_initialize + # @!method include?(item) + # @!macro priority_queue_method_include - # @!method clear - # @!macro priority_queue_method_clear + # @!method length + # @!macro priority_queue_method_length - # @!method delete(item) - # @!macro priority_queue_method_delete + # @!method peek + # @!macro priority_queue_method_peek - # @!method empty? - # @!macro priority_queue_method_empty + # @!method pop + # @!macro priority_queue_method_pop - # @!method include?(item) - # @!macro priority_queue_method_include + # @!method push(item) + # @!macro priority_queue_method_push - # @!method length - # @!macro priority_queue_method_length - - # @!method peek - # @!macro priority_queue_method_peek - - # @!method pop - # @!macro priority_queue_method_pop - - # @!method push(item) - # @!macro priority_queue_method_push - - # @!method self.from_list(list, opts = {}) - # @!macro priority_queue_method_from_list + # @!method self.from_list(list, opts = {}) + # @!macro priority_queue_method_from_list + end end end diff --git a/lib/concurrent/collection/priority_queue_impl/java_priority_queue.rb b/lib/concurrent/collection/priority_queue_impl/java_priority_queue.rb new file mode 100644 index 000000000..1c037be30 --- /dev/null +++ b/lib/concurrent/collection/priority_queue_impl/java_priority_queue.rb @@ -0,0 +1,80 @@ +if Concurrent.on_jruby? + module Concurrent + module Collection + module PriorityQueueImpl + + # @!macro priority_queue + class JavaPriorityQueue + + # @!macro priority_queue_method_initialize + def initialize(opts = {}) + order = opts.fetch(:order, :max) + if [:min, :low].include?(order) + @queue = java.util.PriorityQueue.new(11) # 11 is the default initial capacity + else + @queue = java.util.PriorityQueue.new(11, java.util.Collections.reverseOrder()) + end + end + + # @!macro priority_queue_method_clear + def clear + @queue.clear + true + end + + # @!macro priority_queue_method_delete + def delete(item) + found = false + while @queue.remove(item) do + found = true + end + found + end + + # @!macro priority_queue_method_empty + def empty? + @queue.size == 0 + end + + # @!macro priority_queue_method_include + def include?(item) + @queue.contains(item) + end + alias_method :has_priority?, :include? + + # @!macro priority_queue_method_length + def length + @queue.size + end + alias_method :size, :length + + # @!macro priority_queue_method_peek + def peek + @queue.peek + end + + # @!macro priority_queue_method_pop + def pop + @queue.poll + end + alias_method :deq, :pop + alias_method :shift, :pop + + # @!macro priority_queue_method_push + def push(item) + @queue.add(item) + end + alias_method :<<, :push + alias_method :enq, :push + + # @!macro priority_queue_method_from_list + def self.from_list(list, opts = {}) + queue = new(opts) + list.each{|item| queue << item } + queue + end + end + end + end + end +end diff --git a/lib/concurrent/collection/priority_queue_impl/mutex_priority_queue.rb b/lib/concurrent/collection/priority_queue_impl/mutex_priority_queue.rb new file mode 100644 index 000000000..9834544a1 --- /dev/null +++ b/lib/concurrent/collection/priority_queue_impl/mutex_priority_queue.rb @@ -0,0 +1,225 @@ +module Concurrent + module Collection + module PriorityQueueImpl + + # @!macro [attach] priority_queue + # + # A queue collection in which the elements are sorted based on their + # comparison (spaceship) operator `<=>`. Items are added to the queue + # at a position relative to their priority. On removal the element + # with the "highest" priority is removed. By default the sort order is + # from highest to lowest, but a lowest-to-highest sort order can be + # set on construction. + # + # The API is based on the `Queue` class from the Ruby standard library. + # + # The pure Ruby implementation, `MutexPriorityQueue` uses a heap algorithm + # stored in an array. The algorithm is based on the work of Robert Sedgewick + # and Kevin Wayne. + # + # The JRuby native implementation is a thin wrapper around the standard + # library `java.util.PriorityQueue`. + # + # When running under JRuby the class `PriorityQueue` extends `JavaPriorityQueue`. + # When running under all other interpreters it extends `MutexPriorityQueue`. + # + # @note This implementation is *not* thread safe. + # + # @see http://en.wikipedia.org/wiki/Priority_queue + # @see http://ruby-doc.org/stdlib-2.0.0/libdoc/thread/rdoc/Queue.html + # + # @see http://algs4.cs.princeton.edu/24pq/index.php#2.6 + # @see http://algs4.cs.princeton.edu/24pq/MaxPQ.java.html + # + # @see http://docs.oracle.com/javase/7/docs/api/java/util/PriorityQueue.html + class MutexPriorityQueue + + # @!macro [attach] priority_queue_method_initialize + # + # Create a new priority queue with no items. + # + # @param [Hash] opts the options for creating the queue + # @option opts [Symbol] :order (:max) dictates the order in which items are + # stored: from highest to lowest when `:max` or `:high`; from lowest to + # highest when `:min` or `:low` + def initialize(opts = {}) + order = opts.fetch(:order, :max) + @comparator = [:min, :low].include?(order) ? -1 : 1 + clear + end + + # @!macro [attach] priority_queue_method_clear + # + # Removes all of the elements from this priority queue. + def clear + @queue = [nil] + @length = 0 + true + end + + # @!macro [attach] priority_queue_method_delete + # + # Deletes all items from `self` that are equal to `item`. + # + # @param [Object] item the item to be removed from the queue + # @return [Object] true if the item is found else false + def delete(item) + original_length = @length + k = 1 + while k <= @length + if @queue[k] == item + swap(k, @length) + @length -= 1 + sink(k) + @queue.pop + else + k += 1 + end + end + @length != original_length + end + + # @!macro [attach] priority_queue_method_empty + # + # Returns `true` if `self` contains no elements. + # + # @return [Boolean] true if there are no items in the queue else false + def empty? + size == 0 + end + + # @!macro [attach] priority_queue_method_include + # + # Returns `true` if the given item is present in `self` (that is, if any + # element == `item`), otherwise returns false. + # + # @param [Object] item the item to search for + # + # @return [Boolean] true if the item is found else false + def include?(item) + @queue.include?(item) + end + alias_method :has_priority?, :include? + + # @!macro [attach] priority_queue_method_length + # + # The current length of the queue. + # + # @return [Fixnum] the number of items in the queue + def length + @length + end + alias_method :size, :length + + # @!macro [attach] priority_queue_method_peek + # + # Retrieves, but does not remove, the head of this queue, or returns `nil` + # if this queue is empty. + # + # @return [Object] the head of the queue or `nil` when empty + def peek + @queue[1] + end + + # @!macro [attach] priority_queue_method_pop + # + # Retrieves and removes the head of this queue, or returns `nil` if this + # queue is empty. + # + # @return [Object] the head of the queue or `nil` when empty + def pop + max = @queue[1] + swap(1, @length) + @length -= 1 + sink(1) + @queue.pop + max + end + alias_method :deq, :pop + alias_method :shift, :pop + + # @!macro [attach] priority_queue_method_push + # + # Inserts the specified element into this priority queue. + # + # @param [Object] item the item to insert onto the queue + def push(item) + @length += 1 + @queue << item + swim(@length) + true + end + alias_method :<<, :push + alias_method :enq, :push + + # @!macro [attach] priority_queue_method_from_list + # + # Create a new priority queue from the given list. + # + # @param [Enumerable] list the list to build the queue from + # @param [Hash] opts the options for creating the queue + # + # @return [PriorityQueue] the newly created and populated queue + def self.from_list(list, opts = {}) + queue = new(opts) + list.each{|item| queue << item } + queue + end + + protected + + # Exchange the values at the given indexes within the internal array. + # + # @param [Integer] x the first index to swap + # @param [Integer] y the second index to swap + # + # @!visibility private + def swap(x, y) + temp = @queue[x] + @queue[x] = @queue[y] + @queue[y] = temp + end + + # Are the items at the given indexes ordered based on the priority + # order specified at construction? + # + # @param [Integer] x the first index from which to retrieve a comparable value + # @param [Integer] y the second index from which to retrieve a comparable value + # + # @return [Boolean] true if the two elements are in the correct priority order + # else false + # + # @!visibility private + def ordered?(x, y) + (@queue[x] <=> @queue[y]) == @comparator + end + + # Percolate down to maintain heap invariant. + # + # @param [Integer] k the index at which to start the percolation + # + # @!visibility private + def sink(k) + while (j = (2 * k)) <= @length do + j += 1 if j < @length && ! ordered?(j, j+1) + break if ordered?(k, j) + swap(k, j) + k = j + end + end + + # Percolate up to maintain heap invariant. + # + # @param [Integer] k the index at which to start the percolation + # + # @!visibility private + def swim(k) + while k > 1 && ! ordered?(k/2, k) do + swap(k, k/2) + k = k/2 + end + end + end + end + end +end diff --git a/lib/concurrent/executor/timer_set.rb b/lib/concurrent/executor/timer_set.rb index 156016a9e..da1443b6c 100644 --- a/lib/concurrent/executor/timer_set.rb +++ b/lib/concurrent/executor/timer_set.rb @@ -74,7 +74,7 @@ def kill # @param [Hash] opts the options to create the object with. # @!visibility private def ns_initialize(opts) - @queue = PriorityQueue.new(order: :min) + @queue = Collection::PriorityQueue.new(order: :min) @task_executor = Executor.executor_from_options(opts) || Concurrent.global_io_executor @timer_executor = SingleThreadExecutor.new @condition = Event.new diff --git a/spec/concurrent/collection/priority_queue_spec.rb b/spec/concurrent/collection/priority_queue_spec.rb index ae5b16256..bbed5e02c 100644 --- a/spec/concurrent/collection/priority_queue_spec.rb +++ b/spec/concurrent/collection/priority_queue_spec.rb @@ -287,28 +287,32 @@ end module Concurrent + module Collection - describe MutexPriorityQueue do + module PriorityQueueImpl + describe MutexPriorityQueue do - it_should_behave_like :priority_queue - end + it_should_behave_like :priority_queue + end - if Concurrent.on_jruby? + if Concurrent.on_jruby? - describe JavaPriorityQueue do + describe JavaPriorityQueue do - it_should_behave_like :priority_queue + it_should_behave_like :priority_queue + end + end end - end - describe PriorityQueue do - if Concurrent.on_jruby? - it 'inherits from JavaPriorityQueue' do - expect(PriorityQueue.ancestors).to include(JavaPriorityQueue) - end - else - it 'inherits from MutexPriorityQueue' do - expect(PriorityQueue.ancestors).to include(MutexPriorityQueue) + describe PriorityQueue do + if Concurrent.on_jruby? + it 'inherits from JavaPriorityQueue' do + expect(PriorityQueue.ancestors).to include(Collection::PriorityQueueImpl::JavaPriorityQueue) + end + else + it 'inherits from MutexPriorityQueue' do + expect(PriorityQueue.ancestors).to include(Collection::PriorityQueueImpl::MutexPriorityQueue) + end end end end From 38ae73828a66e8e5c9cb71fd6396254438c6c69c Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Thu, 21 May 2015 17:08:05 -0600 Subject: [PATCH 06/11] Moved Exchanger and LazyRegister to Edge module. --- lib/concurrent-edge.rb | 4 +- lib/concurrent/edge/exchanger.rb | 60 ++++++++++++++++ lib/concurrent/edge/lazy_register.rb | 78 ++++++++++++++++++++ lib/concurrent/exchanger.rb | 56 --------------- lib/concurrent/file_map.rb | 2 - lib/concurrent/lazy_register.rb | 76 -------------------- spec/concurrent/edge/exchanger_spec.rb | 82 ++++++++++++++++++++++ spec/concurrent/edge/lazy_register_spec.rb | 10 +++ spec/concurrent/exchanger_spec.rb | 80 --------------------- spec/concurrent/lazy_register_spec.rb | 7 -- 10 files changed, 232 insertions(+), 223 deletions(-) create mode 100644 lib/concurrent/edge/exchanger.rb create mode 100644 lib/concurrent/edge/lazy_register.rb delete mode 100644 lib/concurrent/exchanger.rb delete mode 100644 lib/concurrent/lazy_register.rb create mode 100644 spec/concurrent/edge/exchanger_spec.rb create mode 100644 spec/concurrent/edge/lazy_register_spec.rb delete mode 100644 spec/concurrent/exchanger_spec.rb delete mode 100644 spec/concurrent/lazy_register_spec.rb diff --git a/lib/concurrent-edge.rb b/lib/concurrent-edge.rb index c6b6af05e..ebb604a17 100644 --- a/lib/concurrent-edge.rb +++ b/lib/concurrent-edge.rb @@ -3,7 +3,7 @@ require 'concurrent/actor' require 'concurrent/agent' require 'concurrent/channel' -require 'concurrent/exchanger' -require 'concurrent/lazy_register' +require 'concurrent/edge/exchanger' require 'concurrent/edge/future' +require 'concurrent/edge/lazy_register' diff --git a/lib/concurrent/edge/exchanger.rb b/lib/concurrent/edge/exchanger.rb new file mode 100644 index 000000000..0ddb13f7e --- /dev/null +++ b/lib/concurrent/edge/exchanger.rb @@ -0,0 +1,60 @@ +require 'concurrent/mvar' + +module Concurrent + module Edge + + # A synchronization point at which threads can pair and swap elements within + # pairs. Each thread presents some object on entry to the exchange method, + # matches with a partner thread, and receives its partner's object on return. + # + # Uses `MVar` to manage synchronization of the individual elements. + # Since `MVar` is also a `Dereferenceable`, the exchanged values support all + # dereferenceable options. The constructor options hash will be passed to + # the `MVar` constructors. + # + # @see Concurrent::MVar + # @see Concurrent::Dereferenceable + # @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Exchanger.html java.util.concurrent.Exchanger + class Exchanger + + EMPTY = Object.new + + # Create a new `Exchanger` object. + # + # @param [Hash] opts the options controlling how the managed references + # will be processed + def initialize(opts = {}) + @first = MVar.new(EMPTY, opts) + @second = MVar.new(MVar::EMPTY, opts) + end + + # Waits for another thread to arrive at this exchange point (unless the + # current thread is interrupted), and then transfers the given object to + # it, receiving its object in return. + # + # @param [Object] value the value to exchange with an other thread + # @param [Numeric] timeout the maximum time in second to wait for one other + # thread. nil (default value) means no timeout + # @return [Object] the value exchanged by the other thread; nil if timed out + def exchange(value, timeout = nil) + first = @first.take(timeout) + if first == MVar::TIMEOUT + nil + elsif first == EMPTY + @first.put value + second = @second.take timeout + if second == MVar::TIMEOUT + nil + else + second + end + else + @first.put EMPTY + @second.put value + first + end + end + + end + end +end diff --git a/lib/concurrent/edge/lazy_register.rb b/lib/concurrent/edge/lazy_register.rb new file mode 100644 index 000000000..185d75b1a --- /dev/null +++ b/lib/concurrent/edge/lazy_register.rb @@ -0,0 +1,78 @@ +require 'concurrent/atomic/atomic_reference' +require 'concurrent/delay' + +module Concurrent + module Edge + + # Hash-like collection that store lazys evaluated values. + # + # @example + # register = Concurrent::LazyRegister.new + # #=> #> + # register[:key] + # #=> nil + # register.add(:key) { Concurrent::Actor.spawn!(Actor::AdHoc, :ping) { -> message { message } } } + # #=> #> + # register[:key] + # #=> # + class LazyRegister + + def initialize + @data = AtomicReference.new(Hash.new) + end + + # Element reference. Retrieves the value object corresponding to the + # key object. Returns nil if the key is not found. Raises an exception + # if the stored item raised an exception when the block was evaluated. + # + # @param [Object] key + # @return [Object] value stored for the key or nil if the key is not found + # + # @raise Exception when the initialization block fails + def [](key) + delay = @data.get[key] + delay ? delay.value! : nil + end + + # Returns true if the given key is present. + # + # @param [Object] key + # @return [true, false] if the key is registered + def registered?(key) + @data.get.key?(key) + end + + alias_method :key?, :registered? + alias_method :has_key?, :registered? + + # Element assignment. Associates the value given by value with the + # key given by key. + # + # @param [Object] key + # @yield the object to store under the key + # + # @return [LazyRegister] self + def register(key, &block) + delay = Delay.new(executor: :immediate, &block) + @data.update { |h| h.merge(key => delay) } + self + end + + alias_method :add, :register + alias_method :store, :register + + # Un-registers the object under key, realized or not. + # + # @param [Object] key + # + # @return [LazyRegister] self + def unregister(key) + @data.update { |h| h.dup.tap { |j| j.delete(key) } } + self + end + + alias_method :remove, :unregister + alias_method :delete, :unregister + end + end +end diff --git a/lib/concurrent/exchanger.rb b/lib/concurrent/exchanger.rb deleted file mode 100644 index f0e83ad14..000000000 --- a/lib/concurrent/exchanger.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Concurrent - - # A synchronization point at which threads can pair and swap elements within - # pairs. Each thread presents some object on entry to the exchange method, - # matches with a partner thread, and receives its partner's object on return. - # - # Uses `MVar` to manage synchronization of the individual elements. - # Since `MVar` is also a `Dereferenceable`, the exchanged values support all - # dereferenceable options. The constructor options hash will be passed to - # the `MVar` constructors. - # - # @see Concurrent::MVar - # @see Concurrent::Dereferenceable - # @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Exchanger.html java.util.concurrent.Exchanger - class Exchanger - - EMPTY = Object.new - - # Create a new `Exchanger` object. - # - # @param [Hash] opts the options controlling how the managed references - # will be processed - def initialize(opts = {}) - @first = MVar.new(EMPTY, opts) - @second = MVar.new(MVar::EMPTY, opts) - end - - # Waits for another thread to arrive at this exchange point (unless the - # current thread is interrupted), and then transfers the given object to - # it, receiving its object in return. - # - # @param [Object] value the value to exchange with an other thread - # @param [Numeric] timeout the maximum time in second to wait for one other - # thread. nil (default value) means no timeout - # @return [Object] the value exchanged by the other thread; nil if timed out - def exchange(value, timeout = nil) - first = @first.take(timeout) - if first == MVar::TIMEOUT - nil - elsif first == EMPTY - @first.put value - second = @second.take timeout - if second == MVar::TIMEOUT - nil - else - second - end - else - @first.put EMPTY - @second.put value - first - end - end - - end -end diff --git a/lib/concurrent/file_map.rb b/lib/concurrent/file_map.rb index 9bada40fc..e22a96fc1 100644 --- a/lib/concurrent/file_map.rb +++ b/lib/concurrent/file_map.rb @@ -7,8 +7,6 @@ module Concurrent 'lib/concurrent/channel.rb', 'lib/concurrent/channel/**/*.rb', 'lib/concurrent/agent.rb', - 'lib/concurrent/exchanger.rb', - 'lib/concurrent/lazy_register.rb', 'lib/concurrent/edge/**/*.rb'] & git_files core_lib_files = all_lib_files - edge_lib_files diff --git a/lib/concurrent/lazy_register.rb b/lib/concurrent/lazy_register.rb deleted file mode 100644 index 0e4b837c0..000000000 --- a/lib/concurrent/lazy_register.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'concurrent/atomic/atomic_reference' -require 'concurrent/delay' - -module Concurrent - - # Hash-like collection that store lazys evaluated values. - # - # @example - # register = Concurrent::LazyRegister.new - # #=> #> - # register[:key] - # #=> nil - # register.add(:key) { Concurrent::Actor.spawn!(Actor::AdHoc, :ping) { -> message { message } } } - # #=> #> - # register[:key] - # #=> # - class LazyRegister - - def initialize - @data = AtomicReference.new(Hash.new) - end - - # Element reference. Retrieves the value object corresponding to the - # key object. Returns nil if the key is not found. Raises an exception - # if the stored item raised an exception when the block was evaluated. - # - # @param [Object] key - # @return [Object] value stored for the key or nil if the key is not found - # - # @raise Exception when the initialization block fails - def [](key) - delay = @data.get[key] - delay ? delay.value! : nil - end - - # Returns true if the given key is present. - # - # @param [Object] key - # @return [true, false] if the key is registered - def registered?(key) - @data.get.key?(key) - end - - alias_method :key?, :registered? - alias_method :has_key?, :registered? - - # Element assignment. Associates the value given by value with the - # key given by key. - # - # @param [Object] key - # @yield the object to store under the key - # - # @return [LazyRegister] self - def register(key, &block) - delay = Delay.new(executor: :immediate, &block) - @data.update { |h| h.merge(key => delay) } - self - end - - alias_method :add, :register - alias_method :store, :register - - # Un-registers the object under key, realized or not. - # - # @param [Object] key - # - # @return [LazyRegister] self - def unregister(key) - @data.update { |h| h.dup.tap { |j| j.delete(key) } } - self - end - - alias_method :remove, :unregister - alias_method :delete, :unregister - end -end diff --git a/spec/concurrent/edge/exchanger_spec.rb b/spec/concurrent/edge/exchanger_spec.rb new file mode 100644 index 000000000..8782a031f --- /dev/null +++ b/spec/concurrent/edge/exchanger_spec.rb @@ -0,0 +1,82 @@ +module Concurrent + module Edge + + describe Exchanger, notravis: true do + + describe 'exchange' do + + context 'without timeout' do + + it 'should block' do + latch_1 = Concurrent::CountDownLatch.new + latch_2 = Concurrent::CountDownLatch.new + + t = Thread.new do + latch_1.count_down + subject.exchange(1) + latch_2.count_down + end + + latch_1.wait(1) + latch_2.wait(0.1) + expect(latch_2.count).to eq 1 + t.kill + end + + it 'should receive the other value' do + first_value = nil + second_value = nil + latch = Concurrent::CountDownLatch.new(2) + + threads = [ + Thread.new { first_value = subject.exchange(2); latch.count_down }, + Thread.new { second_value = subject.exchange(4); latch.count_down } + ] + + latch.wait(1) + + expect(first_value).to eq 4 + expect(second_value).to eq 2 + + threads.each {|t| t.kill } + end + + it 'can be reused' do + first_value = nil + second_value = nil + latch_1 = Concurrent::CountDownLatch.new(2) + latch_2 = Concurrent::CountDownLatch.new(2) + + threads = [ + Thread.new { first_value = subject.exchange(1); latch_1.count_down }, + Thread.new { second_value = subject.exchange(0); latch_1.count_down } + ] + + latch_1.wait(1) + threads.each {|t| t.kill } + + threads = [ + Thread.new { first_value = subject.exchange(10); latch_2.count_down }, + Thread.new { second_value = subject.exchange(12); latch_2.count_down } + ] + + latch_2.wait(1) + expect(first_value).to eq 12 + expect(second_value).to eq 10 + threads.each {|t| t.kill } + end + end + + context 'with timeout' do + + it 'should block until timeout' do + duration = Concurrent::TestHelpers.monotonic_interval do + subject.exchange(2, 0.1) + end + expect(duration).to be_within(0.05).of(0.1) + end + end + end + end + end +end diff --git a/spec/concurrent/edge/lazy_register_spec.rb b/spec/concurrent/edge/lazy_register_spec.rb new file mode 100644 index 000000000..e0f130577 --- /dev/null +++ b/spec/concurrent/edge/lazy_register_spec.rb @@ -0,0 +1,10 @@ +module Concurrent + module Edge + + describe LazyRegister do + + pending + + end + end +end diff --git a/spec/concurrent/exchanger_spec.rb b/spec/concurrent/exchanger_spec.rb deleted file mode 100644 index 381e24eb6..000000000 --- a/spec/concurrent/exchanger_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -module Concurrent - - describe Exchanger, notravis: true do - - describe 'exchange' do - - context 'without timeout' do - - it 'should block' do - latch_1 = Concurrent::CountDownLatch.new - latch_2 = Concurrent::CountDownLatch.new - - t = Thread.new do - latch_1.count_down - subject.exchange(1) - latch_2.count_down - end - - latch_1.wait(1) - latch_2.wait(0.1) - expect(latch_2.count).to eq 1 - t.kill - end - - it 'should receive the other value' do - first_value = nil - second_value = nil - latch = Concurrent::CountDownLatch.new(2) - - threads = [ - Thread.new { first_value = subject.exchange(2); latch.count_down }, - Thread.new { second_value = subject.exchange(4); latch.count_down } - ] - - latch.wait(1) - - expect(first_value).to eq 4 - expect(second_value).to eq 2 - - threads.each {|t| t.kill } - end - - it 'can be reused' do - first_value = nil - second_value = nil - latch_1 = Concurrent::CountDownLatch.new(2) - latch_2 = Concurrent::CountDownLatch.new(2) - - threads = [ - Thread.new { first_value = subject.exchange(1); latch_1.count_down }, - Thread.new { second_value = subject.exchange(0); latch_1.count_down } - ] - - latch_1.wait(1) - threads.each {|t| t.kill } - - threads = [ - Thread.new { first_value = subject.exchange(10); latch_2.count_down }, - Thread.new { second_value = subject.exchange(12); latch_2.count_down } - ] - - latch_2.wait(1) - expect(first_value).to eq 12 - expect(second_value).to eq 10 - threads.each {|t| t.kill } - end - end - - context 'with timeout' do - - it 'should block until timeout' do - duration = Concurrent::TestHelpers.monotonic_interval do - subject.exchange(2, 0.1) - end - expect(duration).to be_within(0.05).of(0.1) - end - end - end - end -end diff --git a/spec/concurrent/lazy_register_spec.rb b/spec/concurrent/lazy_register_spec.rb deleted file mode 100644 index 362535a7c..000000000 --- a/spec/concurrent/lazy_register_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Concurrent - describe LazyRegister do - - pending - - end -end From 5593c6f59f6223370fabde7d3cb7a7d0dabd75e1 Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Thu, 21 May 2015 17:26:03 -0600 Subject: [PATCH 07/11] Moved classes into Utility module. --- lib/concurrent.rb | 10 +- lib/concurrent/at_exit.rb | 89 ------- lib/concurrent/configuration.rb | 2 +- lib/concurrent/executor/executor_service.rb | 2 +- lib/concurrent/utilities.rb | 4 - lib/concurrent/utility/at_exit.rb | 89 +++++++ lib/concurrent/utility/engine.rb | 52 ++-- lib/concurrent/utility/processor_count.rb | 258 ++++++++++---------- lib/concurrent/utility/timeout.rb | 2 + lib/concurrent/utility/timer.rb | 12 +- spec/support/example_group_extensions.rb | 9 +- 11 files changed, 264 insertions(+), 265 deletions(-) delete mode 100644 lib/concurrent/at_exit.rb delete mode 100644 lib/concurrent/utilities.rb create mode 100644 lib/concurrent/utility/at_exit.rb diff --git a/lib/concurrent.rb b/lib/concurrent.rb index 99ac87e1c..8c1048f7a 100644 --- a/lib/concurrent.rb +++ b/lib/concurrent.rb @@ -1,15 +1,10 @@ require 'concurrent/version' -require 'concurrent/synchronization_object' -require 'concurrent/at_exit' - require 'concurrent/configuration' require 'concurrent/atomics' -require 'concurrent/collections' require 'concurrent/errors' require 'concurrent/executors' -require 'concurrent/utilities' require 'concurrent/struct' require 'concurrent/atomic/atomic_reference' @@ -21,9 +16,14 @@ require 'concurrent/mvar' require 'concurrent/promise' require 'concurrent/scheduled_task' +require 'concurrent/synchronization_object' require 'concurrent/timer_task' require 'concurrent/tvar' +# deprecated +require 'concurrent/utility/timeout' +require 'concurrent/utility/timer' + # @!macro [new] monotonic_clock_warning # # @note Time calculations one all platforms and languages are sensitive to diff --git a/lib/concurrent/at_exit.rb b/lib/concurrent/at_exit.rb deleted file mode 100644 index 5547d93d4..000000000 --- a/lib/concurrent/at_exit.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'concurrent/concern/logging' -require 'concurrent/synchronization_object' - -module Concurrent - - # Provides ability to add and remove handlers to be run at `Kernel#at_exit`, order is undefined. - # Each handler is executed at most once. - class AtExitImplementation < SynchronizationObject - include Concern::Logging - - # Add a handler to be run at `Kernel#at_exit` - # @param [Object] handler_id optionally provide an id, if allready present, handler is replaced - # @yield the handler - # @return id of the handler - def add(handler_id = nil, &handler) - id = handler_id || handler.object_id - synchronize { @handlers[id] = handler } - id - end - - # Delete a handler by handler_id - # @return [true, false] - def delete(handler_id) - !!synchronize { @handlers.delete handler_id } - end - - # Is handler with handler_id rpesent? - # @return [true, false] - def handler?(handler_id) - synchronize { @handlers.key? handler_id } - end - - # @return copy of the handlers - def handlers - synchronize { @handlers }.clone - end - - # install `Kernel#at_exit` callback to execute added handlers - def install - synchronize do - @installed ||= begin - at_exit { runner } - true - end - self - end - end - - # Will it run during `Kernel#at_exit` - def enabled? - synchronize { @enabled } - end - - # Configure if it runs during `Kernel#at_exit` - def enabled=(value) - synchronize { @enabled = value } - end - - # run the handlers manually - # @return ids of the handlers - def run - handlers, _ = synchronize { handlers, @handlers = @handlers, {} } - handlers.each do |_, handler| - begin - handler.call - rescue => error - log ERROR, error - end - end - handlers.keys - end - - private - - def ns_initialize(enabled = true) - @handlers = {} - @enabled = enabled - end - - def runner - run if synchronize { @enabled } - end - end - - private_constant :AtExitImplementation - - # @see AtExitImplementation - AtExit = AtExitImplementation.new.install -end diff --git a/lib/concurrent/configuration.rb b/lib/concurrent/configuration.rb index ec926deb6..46c85ec90 100644 --- a/lib/concurrent/configuration.rb +++ b/lib/concurrent/configuration.rb @@ -1,9 +1,9 @@ require 'thread' require 'concurrent/atomics' require 'concurrent/errors' -require 'concurrent/at_exit' require 'concurrent/executors' require 'concurrent/concern/logging' +require 'concurrent/utility/at_exit' require 'concurrent/utility/processor_count' module Concurrent diff --git a/lib/concurrent/executor/executor_service.rb b/lib/concurrent/executor/executor_service.rb index 3330166f8..ce9b9abad 100644 --- a/lib/concurrent/executor/executor_service.rb +++ b/lib/concurrent/executor/executor_service.rb @@ -1,6 +1,6 @@ require 'concurrent/errors' require 'concurrent/concern/logging' -require 'concurrent/at_exit' +require 'concurrent/utility/at_exit' require 'concurrent/atomic/event' require 'concurrent/synchronization_object' diff --git a/lib/concurrent/utilities.rb b/lib/concurrent/utilities.rb deleted file mode 100644 index 9f5d8f0bb..000000000 --- a/lib/concurrent/utilities.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'concurrent/utility/monotonic_time' -require 'concurrent/utility/processor_count' -require 'concurrent/utility/timeout' -require 'concurrent/utility/timer' diff --git a/lib/concurrent/utility/at_exit.rb b/lib/concurrent/utility/at_exit.rb new file mode 100644 index 000000000..a7aded350 --- /dev/null +++ b/lib/concurrent/utility/at_exit.rb @@ -0,0 +1,89 @@ +require 'concurrent/concern/logging' +require 'concurrent/synchronization_object' + +module Concurrent + module Utility + + # Provides ability to add and remove handlers to be run at `Kernel#at_exit`, order is undefined. + # Each handler is executed at most once. + class AtExitImplementation < SynchronizationObject + include Concern::Logging + + # Add a handler to be run at `Kernel#at_exit` + # @param [Object] handler_id optionally provide an id, if allready present, handler is replaced + # @yield the handler + # @return id of the handler + def add(handler_id = nil, &handler) + id = handler_id || handler.object_id + synchronize { @handlers[id] = handler } + id + end + + # Delete a handler by handler_id + # @return [true, false] + def delete(handler_id) + !!synchronize { @handlers.delete handler_id } + end + + # Is handler with handler_id rpesent? + # @return [true, false] + def handler?(handler_id) + synchronize { @handlers.key? handler_id } + end + + # @return copy of the handlers + def handlers + synchronize { @handlers }.clone + end + + # install `Kernel#at_exit` callback to execute added handlers + def install + synchronize do + @installed ||= begin + at_exit { runner } + true + end + self + end + end + + # Will it run during `Kernel#at_exit` + def enabled? + synchronize { @enabled } + end + + # Configure if it runs during `Kernel#at_exit` + def enabled=(value) + synchronize { @enabled = value } + end + + # run the handlers manually + # @return ids of the handlers + def run + handlers, _ = synchronize { handlers, @handlers = @handlers, {} } + handlers.each do |_, handler| + begin + handler.call + rescue => error + log ERROR, error + end + end + handlers.keys + end + + private + + def ns_initialize(enabled = true) + @handlers = {} + @enabled = enabled + end + + def runner + run if synchronize { @enabled } + end + end + end + + # @see AtExitImplementation + AtExit = Utility::AtExitImplementation.new.install +end diff --git a/lib/concurrent/utility/engine.rb b/lib/concurrent/utility/engine.rb index fbe0b14b7..17f55c27b 100644 --- a/lib/concurrent/utility/engine.rb +++ b/lib/concurrent/utility/engine.rb @@ -1,36 +1,38 @@ module Concurrent + module Utility - module EngineDetector - def on_jruby? - ruby_engine == 'jruby' - end + module EngineDetector + def on_jruby? + ruby_engine == 'jruby' + end - def on_jruby_9000? - on_jruby? && 0 == (JRUBY_VERSION =~ /^9\.0\.0\.0/) - end + def on_jruby_9000? + on_jruby? && 0 == (JRUBY_VERSION =~ /^9\.0\.0\.0/) + end - def on_cruby? - ruby_engine == 'ruby' - end + def on_cruby? + ruby_engine == 'ruby' + end - def on_rbx? - ruby_engine == 'rbx' - end + def on_rbx? + ruby_engine == 'rbx' + end - def ruby_engine - defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby' - end + def ruby_engine + defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby' + end - def ruby_version(comparison, major, minor = 0, patch = 0) - result = (RUBY_VERSION.split('.').map(&:to_i) <=> [major, minor, patch]) - comparisons = { :== => [0], - :>= => [1, 0], - :<= => [-1, 0], - :> => [1], - :< => [-1] } - comparisons.fetch(comparison).include? result + def ruby_version(comparison, major, minor = 0, patch = 0) + result = (RUBY_VERSION.split('.').map(&:to_i) <=> [major, minor, patch]) + comparisons = { :== => [0], + :>= => [1, 0], + :<= => [-1, 0], + :> => [1], + :< => [-1] } + comparisons.fetch(comparison).include? result + end end end - extend EngineDetector + extend Utility::EngineDetector end diff --git a/lib/concurrent/utility/processor_count.rb b/lib/concurrent/utility/processor_count.rb index 56dccb20e..ca853db9a 100644 --- a/lib/concurrent/utility/processor_count.rb +++ b/lib/concurrent/utility/processor_count.rb @@ -1,154 +1,156 @@ require 'rbconfig' require 'concurrent/delay' +require 'concurrent/utility/engine' module Concurrent + module Utility - class ProcessorCounter - def initialize - @processor_count = Delay.new { compute_processor_count } - @physical_processor_count = Delay.new { compute_physical_processor_count } - end + class ProcessorCounter + def initialize + @processor_count = Delay.new { compute_processor_count } + @physical_processor_count = Delay.new { compute_physical_processor_count } + end - # Number of processors seen by the OS and used for process scheduling. For - # performance reasons the calculated value will be memoized on the first - # call. - # - # When running under JRuby the Java runtime call - # `java.lang.Runtime.getRuntime.availableProcessors` will be used. According - # to the Java documentation this "value may change during a particular - # invocation of the virtual machine... [applications] should therefore - # occasionally poll this property." Subsequently the result will NOT be - # memoized under JRuby. - # - # On Windows the Win32 API will be queried for the - # `NumberOfLogicalProcessors from Win32_Processor`. This will return the - # total number "logical processors for the current instance of the - # processor", which taked into account hyperthreading. - # - # * AIX: /usr/sbin/pmcycles (AIX 5+), /usr/sbin/lsdev - # * BSD: /sbin/sysctl - # * Cygwin: /proc/cpuinfo - # * Darwin: /usr/bin/hwprefs, /usr/sbin/sysctl - # * HP-UX: /usr/sbin/ioscan - # * IRIX: /usr/sbin/sysconf - # * Linux: /proc/cpuinfo - # * Minix 3+: /proc/cpuinfo - # * Solaris: /usr/sbin/psrinfo - # * Tru64 UNIX: /usr/sbin/psrinfo - # * UnixWare: /usr/sbin/psrinfo - # - # @return [Integer] number of processors seen by the OS or Java runtime - # - # @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb - # - # @see http://docs.oracle.com/javase/6/docs/api/java/lang/Runtime.html#availableProcessors() - # @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx - def processor_count - @processor_count.value - end + # Number of processors seen by the OS and used for process scheduling. For + # performance reasons the calculated value will be memoized on the first + # call. + # + # When running under JRuby the Java runtime call + # `java.lang.Runtime.getRuntime.availableProcessors` will be used. According + # to the Java documentation this "value may change during a particular + # invocation of the virtual machine... [applications] should therefore + # occasionally poll this property." Subsequently the result will NOT be + # memoized under JRuby. + # + # On Windows the Win32 API will be queried for the + # `NumberOfLogicalProcessors from Win32_Processor`. This will return the + # total number "logical processors for the current instance of the + # processor", which taked into account hyperthreading. + # + # * AIX: /usr/sbin/pmcycles (AIX 5+), /usr/sbin/lsdev + # * BSD: /sbin/sysctl + # * Cygwin: /proc/cpuinfo + # * Darwin: /usr/bin/hwprefs, /usr/sbin/sysctl + # * HP-UX: /usr/sbin/ioscan + # * IRIX: /usr/sbin/sysconf + # * Linux: /proc/cpuinfo + # * Minix 3+: /proc/cpuinfo + # * Solaris: /usr/sbin/psrinfo + # * Tru64 UNIX: /usr/sbin/psrinfo + # * UnixWare: /usr/sbin/psrinfo + # + # @return [Integer] number of processors seen by the OS or Java runtime + # + # @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb + # + # @see http://docs.oracle.com/javase/6/docs/api/java/lang/Runtime.html#availableProcessors() + # @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx + def processor_count + @processor_count.value + end - # Number of physical processor cores on the current system. For performance - # reasons the calculated value will be memoized on the first call. - # - # On Windows the Win32 API will be queried for the `NumberOfCores from - # Win32_Processor`. This will return the total number "of cores for the - # current instance of the processor." On Unix-like operating systems either - # the `hwprefs` or `sysctl` utility will be called in a subshell and the - # returned value will be used. In the rare case where none of these methods - # work or an exception is raised the function will simply return 1. - # - # @return [Integer] number physical processor cores on the current system - # - # @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb - # - # @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx - # @see http://www.unix.com/man-page/osx/1/HWPREFS/ - # @see http://linux.die.net/man/8/sysctl - def physical_processor_count - @physical_processor_count.value - end + # Number of physical processor cores on the current system. For performance + # reasons the calculated value will be memoized on the first call. + # + # On Windows the Win32 API will be queried for the `NumberOfCores from + # Win32_Processor`. This will return the total number "of cores for the + # current instance of the processor." On Unix-like operating systems either + # the `hwprefs` or `sysctl` utility will be called in a subshell and the + # returned value will be used. In the rare case where none of these methods + # work or an exception is raised the function will simply return 1. + # + # @return [Integer] number physical processor cores on the current system + # + # @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb + # + # @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx + # @see http://www.unix.com/man-page/osx/1/HWPREFS/ + # @see http://linux.die.net/man/8/sysctl + def physical_processor_count + @physical_processor_count.value + end - private + private - def compute_processor_count - if Concurrent.on_jruby? - java.lang.Runtime.getRuntime.availableProcessors - else - os_name = RbConfig::CONFIG["target_os"] - if os_name =~ /mingw|mswin/ - require 'win32ole' - result = WIN32OLE.connect("winmgmts://").ExecQuery( - "select NumberOfLogicalProcessors from Win32_Processor") - result.to_enum.collect(&:NumberOfLogicalProcessors).reduce(:+) - elsif File.readable?("/proc/cpuinfo") - IO.read("/proc/cpuinfo").scan(/^processor/).size - elsif File.executable?("/usr/bin/hwprefs") - IO.popen("/usr/bin/hwprefs thread_count").read.to_i - elsif File.executable?("/usr/sbin/psrinfo") - IO.popen("/usr/sbin/psrinfo").read.scan(/^.*on-*line/).size - elsif File.executable?("/usr/sbin/ioscan") - IO.popen("/usr/sbin/ioscan -kC processor") do |out| - out.read.scan(/^.*processor/).size - end - elsif File.executable?("/usr/sbin/pmcycles") - IO.popen("/usr/sbin/pmcycles -m").read.count("\n") - elsif File.executable?("/usr/sbin/lsdev") - IO.popen("/usr/sbin/lsdev -Cc processor -S 1").read.count("\n") - elsif File.executable?("/usr/sbin/sysconf") and os_name =~ /irix/i - IO.popen("/usr/sbin/sysconf NPROC_ONLN").read.to_i - elsif File.executable?("/usr/sbin/sysctl") - IO.popen("/usr/sbin/sysctl -n hw.ncpu").read.to_i - elsif File.executable?("/sbin/sysctl") - IO.popen("/sbin/sysctl -n hw.ncpu").read.to_i + def compute_processor_count + if Concurrent.on_jruby? + java.lang.Runtime.getRuntime.availableProcessors else - 1 + os_name = RbConfig::CONFIG["target_os"] + if os_name =~ /mingw|mswin/ + require 'win32ole' + result = WIN32OLE.connect("winmgmts://").ExecQuery( + "select NumberOfLogicalProcessors from Win32_Processor") + result.to_enum.collect(&:NumberOfLogicalProcessors).reduce(:+) + elsif File.readable?("/proc/cpuinfo") + IO.read("/proc/cpuinfo").scan(/^processor/).size + elsif File.executable?("/usr/bin/hwprefs") + IO.popen("/usr/bin/hwprefs thread_count").read.to_i + elsif File.executable?("/usr/sbin/psrinfo") + IO.popen("/usr/sbin/psrinfo").read.scan(/^.*on-*line/).size + elsif File.executable?("/usr/sbin/ioscan") + IO.popen("/usr/sbin/ioscan -kC processor") do |out| + out.read.scan(/^.*processor/).size + end + elsif File.executable?("/usr/sbin/pmcycles") + IO.popen("/usr/sbin/pmcycles -m").read.count("\n") + elsif File.executable?("/usr/sbin/lsdev") + IO.popen("/usr/sbin/lsdev -Cc processor -S 1").read.count("\n") + elsif File.executable?("/usr/sbin/sysconf") and os_name =~ /irix/i + IO.popen("/usr/sbin/sysconf NPROC_ONLN").read.to_i + elsif File.executable?("/usr/sbin/sysctl") + IO.popen("/usr/sbin/sysctl -n hw.ncpu").read.to_i + elsif File.executable?("/sbin/sysctl") + IO.popen("/sbin/sysctl -n hw.ncpu").read.to_i + else + 1 + end end + rescue + return 1 end - rescue - return 1 - end - def compute_physical_processor_count - ppc = case RbConfig::CONFIG["target_os"] - when /darwin1/ - IO.popen("/usr/sbin/sysctl -n hw.physicalcpu").read.to_i - when /linux/ - cores = {} # unique physical ID / core ID combinations - phy = 0 - IO.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln| - if ln.start_with?("physical") - phy = ln[/\d+/] - elsif ln.start_with?("core") - cid = phy + ":" + ln[/\d+/] - cores[cid] = true if not cores[cid] + def compute_physical_processor_count + ppc = case RbConfig::CONFIG["target_os"] + when /darwin1/ + IO.popen("/usr/sbin/sysctl -n hw.physicalcpu").read.to_i + when /linux/ + cores = {} # unique physical ID / core ID combinations + phy = 0 + IO.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln| + if ln.start_with?("physical") + phy = ln[/\d+/] + elsif ln.start_with?("core") + cid = phy + ":" + ln[/\d+/] + cores[cid] = true if not cores[cid] + end end + cores.count + when /mswin|mingw/ + require 'win32ole' + result_set = WIN32OLE.connect("winmgmts://").ExecQuery( + "select NumberOfCores from Win32_Processor") + result_set.to_enum.collect(&:NumberOfCores).reduce(:+) + else + processor_count end - cores.count - when /mswin|mingw/ - require 'win32ole' - result_set = WIN32OLE.connect("winmgmts://").ExecQuery( - "select NumberOfCores from Win32_Processor") - result_set.to_enum.collect(&:NumberOfCores).reduce(:+) - else - processor_count - end - # fall back to logical count if physical info is invalid - ppc > 0 ? ppc : processor_count - rescue - return 1 + # fall back to logical count if physical info is invalid + ppc > 0 ? ppc : processor_count + rescue + return 1 + end end end # create the default ProcessorCounter on load - @processor_counter = ProcessorCounter.new - singleton_class.send :attr_reader, :processor_counter + PROCESSOR_COUNTER = Utility::ProcessorCounter.new + private_constant :PROCESSOR_COUNTER def self.processor_count - processor_counter.processor_count + PROCESSOR_COUNTER.processor_count end def self.physical_processor_count - processor_counter.physical_processor_count + PROCESSOR_COUNTER.physical_processor_count end - end diff --git a/lib/concurrent/utility/timeout.rb b/lib/concurrent/utility/timeout.rb index 68ba8589b..99ee0b017 100644 --- a/lib/concurrent/utility/timeout.rb +++ b/lib/concurrent/utility/timeout.rb @@ -20,6 +20,8 @@ module Concurrent # @see http://ruby-doc.org/stdlib-2.2.0/libdoc/timeout/rdoc/Timeout.html Ruby Timeout::timeout # # @!macro monotonic_clock_warning + # + # @deprecated timeout is deprecated and will be removed def timeout(seconds, &block) warn '[DEPRECATED] timeout is deprecated and will be removed' diff --git a/lib/concurrent/utility/timer.rb b/lib/concurrent/utility/timer.rb index 50c70e1ca..b46576b70 100644 --- a/lib/concurrent/utility/timer.rb +++ b/lib/concurrent/utility/timer.rb @@ -1,13 +1,9 @@ require 'concurrent/configuration' -require 'thread' module Concurrent - # Perform the given operation asynchronously after the given number of seconds. - # - # This is a convenience method for posting tasks to the global timer set. - # It is intended to be simple and easy to use. For greater control use - # either `TimerSet` or `ScheduledTask` directly. + # [DEPRECATED] Perform the given operation asynchronously after + # the given number of seconds. # # @param [Fixnum] seconds the interval in seconds to wait before executing the task # @@ -16,10 +12,10 @@ module Concurrent # @return [Concurrent::ScheduledTask] IVar representing the task # # @see Concurrent::ScheduledTask - # @see Concurrent::TimerSet # - # @!macro monotonic_clock_warning + # @deprecated use `ScheduledTask` instead def timer(seconds, *args, &block) + warn '[DEPRECATED] use ScheduledTask instead' raise ArgumentError.new('no block given') unless block_given? raise ArgumentError.new('interval must be greater than or equal to zero') if seconds < 0 Concurrent.configuration.global_timer_set.post(seconds, *args, &block) diff --git a/spec/support/example_group_extensions.rb b/spec/support/example_group_extensions.rb index 06d746351..15edb49fa 100644 --- a/spec/support/example_group_extensions.rb +++ b/spec/support/example_group_extensions.rb @@ -1,5 +1,6 @@ require 'rbconfig' require 'concurrent/native_extensions' +require 'concurrent/utility/engine' module Concurrent module TestHelpers @@ -13,7 +14,7 @@ def delta(v1, v2) return (v1 - v2).abs end - include EngineDetector + include Utility::EngineDetector def use_c_extensions? Concurrent.allow_c_extensions? # from extension_helper.rb @@ -24,9 +25,9 @@ def do_no_reset! end GLOBAL_EXECUTORS = [ - [:GLOBAL_FAST_EXECUTOR, -> { Delay.new { Concurrent.new_fast_executor(auto_terminate: true) } }], - [:GLOBAL_IO_EXECUTOR, -> { Delay.new { Concurrent.new_io_executor(auto_terminate: true) } }], - [:GLOBAL_TIMER_SET, -> { Delay.new { Concurrent::TimerSet.new(auto_terminate: true) } }], + [:GLOBAL_FAST_EXECUTOR, -> { Delay.new { Concurrent.new_fast_executor(auto_terminate: true) } }], + [:GLOBAL_IO_EXECUTOR, -> { Delay.new { Concurrent.new_io_executor(auto_terminate: true) } }], + [:GLOBAL_TIMER_SET, -> { Delay.new { Concurrent::TimerSet.new(auto_terminate: true) } }], ] @@killed = false From c6bc2917f95badeec6f40db42c7cad4a6c3963a0 Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Thu, 21 May 2015 17:39:24 -0600 Subject: [PATCH 08/11] Fixed Yardoc --- doc/synchronization.md | 10 +- lib/concurrent/agent.rb | 13 +-- lib/concurrent/collection/priority_queue.rb | 101 ++++++++++++++++-- .../mutex_priority_queue.rb | 101 ++---------------- lib/concurrent/delay.rb | 13 ++- .../abstract_object.rb | 7 +- 6 files changed, 123 insertions(+), 122 deletions(-) diff --git a/doc/synchronization.md b/doc/synchronization.md index 549a01ffb..ae5319cd3 100644 --- a/doc/synchronization.md +++ b/doc/synchronization.md @@ -19,7 +19,7 @@ Provides common parent for all objects which need to be synchronized or be using Example of a simple counter which can be used by multiple threads: ```ruby -class SafeCounter < Concurrent::Synchronization::Object +class SafeCounter < Concurrent::SynchronizationObject def initialize super synchronize { @count = 0 } @@ -60,10 +60,10 @@ Sometimes while already inside the synchronized block some condition is not met. To fulfill these needs there are private methods: -- `ns_wait` {include:Concurrent::Synchronization::AbstractObject#ns_wait} -- `ns_wait_until` {include:Concurrent::Synchronization::AbstractObject#ns_wait_until} -- `ns_signal` {include:Concurrent::Synchronization::AbstractObject#ns_signal} -- `ns_broadcast` {include:Concurrent::Synchronization::AbstractObject#ns_broadcast} +- `ns_wait` {include:Concurrent::SynchronizationObjectImpl::AbstractObject#ns_wait} +- `ns_wait_until` {include:Concurrent::SynchronizationObjectImpl::AbstractObject#ns_wait_until} +- `ns_signal` {include:Concurrent::SynchronizationObjectImpl::AbstractObject#ns_signal} +- `ns_broadcast` {include:Concurrent::SynchronizationObjectImpl::AbstractObject#ns_broadcast} All methods have to be called inside synchronized block. diff --git a/lib/concurrent/agent.rb b/lib/concurrent/agent.rb index 1a3388240..938ae0029 100644 --- a/lib/concurrent/agent.rb +++ b/lib/concurrent/agent.rb @@ -21,18 +21,7 @@ class Agent # # @param [Object] initial the initial value # - # @!macro [attach] executor_and_deref_options - # - # @param [Hash] opts the options used to define the behavior at update and deref - # and to specify the executor on which to perform actions - # @option opts [Executor] :executor when set use the given `Executor` instance. - # Three special values are also supported: `:task` returns the global task pool, - # `:operation` returns the global operation pool, and `:immediate` returns a new - # `ImmediateExecutor` object. - # @option opts [Boolean] :dup_on_deref (false) call `#dup` before returning the data - # @option opts [Boolean] :freeze_on_deref (false) call `#freeze` before returning the data - # @option opts [Proc] :copy_on_deref (nil) call the given `Proc` passing - # the internal value and returning the value returned from the proc + # @!macro executor_and_deref_options def initialize(initial, opts = {}) @value = initial @rescuers = [] diff --git a/lib/concurrent/collection/priority_queue.rb b/lib/concurrent/collection/priority_queue.rb index d609a1688..3453e0596 100644 --- a/lib/concurrent/collection/priority_queue.rb +++ b/lib/concurrent/collection/priority_queue.rb @@ -13,7 +13,36 @@ module PriorityQueueImpl end end - # @!macro priority_queue + # @!macro [attach] priority_queue + # + # A queue collection in which the elements are sorted based on their + # comparison (spaceship) operator `<=>`. Items are added to the queue + # at a position relative to their priority. On removal the element + # with the "highest" priority is removed. By default the sort order is + # from highest to lowest, but a lowest-to-highest sort order can be + # set on construction. + # + # The API is based on the `Queue` class from the Ruby standard library. + # + # The pure Ruby implementation, `MutexPriorityQueue` uses a heap algorithm + # stored in an array. The algorithm is based on the work of Robert Sedgewick + # and Kevin Wayne. + # + # The JRuby native implementation is a thin wrapper around the standard + # library `java.util.PriorityQueue`. + # + # When running under JRuby the class `PriorityQueue` extends `JavaPriorityQueue`. + # When running under all other interpreters it extends `MutexPriorityQueue`. + # + # @note This implementation is *not* thread safe. + # + # @see http://en.wikipedia.org/wiki/Priority_queue + # @see http://ruby-doc.org/stdlib-2.0.0/libdoc/thread/rdoc/Queue.html + # + # @see http://algs4.cs.princeton.edu/24pq/index.php#2.6 + # @see http://algs4.cs.princeton.edu/24pq/MaxPQ.java.html + # + # @see http://docs.oracle.com/javase/7/docs/api/java/util/PriorityQueue.html class PriorityQueue < PriorityQueueImpl::Implementation alias_method :has_priority?, :include? @@ -27,34 +56,84 @@ class PriorityQueue < PriorityQueueImpl::Implementation alias_method :enq, :push # @!method initialize(opts = {}) - # @!macro priority_queue_method_initialize + # @!macro [attach] priority_queue_method_initialize + # + # Create a new priority queue with no items. + # + # @param [Hash] opts the options for creating the queue + # @option opts [Symbol] :order (:max) dictates the order in which items are + # stored: from highest to lowest when `:max` or `:high`; from lowest to + # highest when `:min` or `:low` # @!method clear - # @!macro priority_queue_method_clear + # @!macro [attach] priority_queue_method_clear + # + # Removes all of the elements from this priority queue. # @!method delete(item) - # @!macro priority_queue_method_delete + # @!macro [attach] priority_queue_method_delete + # + # Deletes all items from `self` that are equal to `item`. + # + # @param [Object] item the item to be removed from the queue + # @return [Object] true if the item is found else false # @!method empty? - # @!macro priority_queue_method_empty + # @!macro [attach] priority_queue_method_empty + # + # Returns `true` if `self` contains no elements. + # + # @return [Boolean] true if there are no items in the queue else false # @!method include?(item) - # @!macro priority_queue_method_include + # @!macro [attach] priority_queue_method_include + # + # Returns `true` if the given item is present in `self` (that is, if any + # element == `item`), otherwise returns false. + # + # @param [Object] item the item to search for + # + # @return [Boolean] true if the item is found else false # @!method length - # @!macro priority_queue_method_length + # @!macro [attach] priority_queue_method_length + # + # The current length of the queue. + # + # @return [Fixnum] the number of items in the queue # @!method peek - # @!macro priority_queue_method_peek + # @!macro [attach] priority_queue_method_peek + # + # Retrieves, but does not remove, the head of this queue, or returns `nil` + # if this queue is empty. + # + # @return [Object] the head of the queue or `nil` when empty # @!method pop - # @!macro priority_queue_method_pop + # @!macro [attach] priority_queue_method_pop + # + # Retrieves and removes the head of this queue, or returns `nil` if this + # queue is empty. + # + # @return [Object] the head of the queue or `nil` when empty # @!method push(item) - # @!macro priority_queue_method_push + # @!macro [attach] priority_queue_method_push + # + # Inserts the specified element into this priority queue. + # + # @param [Object] item the item to insert onto the queue # @!method self.from_list(list, opts = {}) - # @!macro priority_queue_method_from_list + # @!macro [attach] priority_queue_method_from_list + # + # Create a new priority queue from the given list. + # + # @param [Enumerable] list the list to build the queue from + # @param [Hash] opts the options for creating the queue + # + # @return [PriorityQueue] the newly created and populated queue end end end diff --git a/lib/concurrent/collection/priority_queue_impl/mutex_priority_queue.rb b/lib/concurrent/collection/priority_queue_impl/mutex_priority_queue.rb index 9834544a1..f122daecf 100644 --- a/lib/concurrent/collection/priority_queue_impl/mutex_priority_queue.rb +++ b/lib/concurrent/collection/priority_queue_impl/mutex_priority_queue.rb @@ -2,67 +2,24 @@ module Concurrent module Collection module PriorityQueueImpl - # @!macro [attach] priority_queue - # - # A queue collection in which the elements are sorted based on their - # comparison (spaceship) operator `<=>`. Items are added to the queue - # at a position relative to their priority. On removal the element - # with the "highest" priority is removed. By default the sort order is - # from highest to lowest, but a lowest-to-highest sort order can be - # set on construction. - # - # The API is based on the `Queue` class from the Ruby standard library. - # - # The pure Ruby implementation, `MutexPriorityQueue` uses a heap algorithm - # stored in an array. The algorithm is based on the work of Robert Sedgewick - # and Kevin Wayne. - # - # The JRuby native implementation is a thin wrapper around the standard - # library `java.util.PriorityQueue`. - # - # When running under JRuby the class `PriorityQueue` extends `JavaPriorityQueue`. - # When running under all other interpreters it extends `MutexPriorityQueue`. - # - # @note This implementation is *not* thread safe. - # - # @see http://en.wikipedia.org/wiki/Priority_queue - # @see http://ruby-doc.org/stdlib-2.0.0/libdoc/thread/rdoc/Queue.html - # - # @see http://algs4.cs.princeton.edu/24pq/index.php#2.6 - # @see http://algs4.cs.princeton.edu/24pq/MaxPQ.java.html - # - # @see http://docs.oracle.com/javase/7/docs/api/java/util/PriorityQueue.html + # @!macro priority_queue class MutexPriorityQueue - # @!macro [attach] priority_queue_method_initialize - # - # Create a new priority queue with no items. - # - # @param [Hash] opts the options for creating the queue - # @option opts [Symbol] :order (:max) dictates the order in which items are - # stored: from highest to lowest when `:max` or `:high`; from lowest to - # highest when `:min` or `:low` + # @!macro priority_queue_method_initialize def initialize(opts = {}) order = opts.fetch(:order, :max) @comparator = [:min, :low].include?(order) ? -1 : 1 clear end - # @!macro [attach] priority_queue_method_clear - # - # Removes all of the elements from this priority queue. + # @!macro priority_queue_method_clear def clear @queue = [nil] @length = 0 true end - # @!macro [attach] priority_queue_method_delete - # - # Deletes all items from `self` that are equal to `item`. - # - # @param [Object] item the item to be removed from the queue - # @return [Object] true if the item is found else false + # @!macro priority_queue_method_delete def delete(item) original_length = @length k = 1 @@ -79,54 +36,29 @@ def delete(item) @length != original_length end - # @!macro [attach] priority_queue_method_empty - # - # Returns `true` if `self` contains no elements. - # - # @return [Boolean] true if there are no items in the queue else false + # @!macro priority_queue_method_empty def empty? size == 0 end - # @!macro [attach] priority_queue_method_include - # - # Returns `true` if the given item is present in `self` (that is, if any - # element == `item`), otherwise returns false. - # - # @param [Object] item the item to search for - # - # @return [Boolean] true if the item is found else false + # @!macro priority_queue_method_include def include?(item) @queue.include?(item) end alias_method :has_priority?, :include? - # @!macro [attach] priority_queue_method_length - # - # The current length of the queue. - # - # @return [Fixnum] the number of items in the queue + # @!macro priority_queue_method_length def length @length end alias_method :size, :length - # @!macro [attach] priority_queue_method_peek - # - # Retrieves, but does not remove, the head of this queue, or returns `nil` - # if this queue is empty. - # - # @return [Object] the head of the queue or `nil` when empty + # @!macro priority_queue_method_peek def peek @queue[1] end - # @!macro [attach] priority_queue_method_pop - # - # Retrieves and removes the head of this queue, or returns `nil` if this - # queue is empty. - # - # @return [Object] the head of the queue or `nil` when empty + # @!macro priority_queue_method_pop def pop max = @queue[1] swap(1, @length) @@ -138,11 +70,7 @@ def pop alias_method :deq, :pop alias_method :shift, :pop - # @!macro [attach] priority_queue_method_push - # - # Inserts the specified element into this priority queue. - # - # @param [Object] item the item to insert onto the queue + # @!macro priority_queue_method_push def push(item) @length += 1 @queue << item @@ -152,14 +80,7 @@ def push(item) alias_method :<<, :push alias_method :enq, :push - # @!macro [attach] priority_queue_method_from_list - # - # Create a new priority queue from the given list. - # - # @param [Enumerable] list the list to build the queue from - # @param [Hash] opts the options for creating the queue - # - # @return [PriorityQueue] the newly created and populated queue + # @!macro priority_queue_method_from_list def self.from_list(list, opts = {}) queue = new(opts) list.each{|item| queue << item } diff --git a/lib/concurrent/delay.rb b/lib/concurrent/delay.rb index 481ec0b43..ff718a96d 100644 --- a/lib/concurrent/delay.rb +++ b/lib/concurrent/delay.rb @@ -51,7 +51,18 @@ class Delay < SynchronizationObject # Create a new `Delay` in the `:pending` state. # - # @!macro executor_and_deref_options + # @!macro [attach] executor_and_deref_options + # + # @param [Hash] opts the options used to define the behavior at update and deref + # and to specify the executor on which to perform actions + # @option opts [Executor] :executor when set use the given `Executor` instance. + # Three special values are also supported: `:task` returns the global task pool, + # `:operation` returns the global operation pool, and `:immediate` returns a new + # `ImmediateExecutor` object. + # @option opts [Boolean] :dup_on_deref (false) call `#dup` before returning the data + # @option opts [Boolean] :freeze_on_deref (false) call `#freeze` before returning the data + # @option opts [Proc] :copy_on_deref (nil) call the given `Proc` passing + # the internal value and returning the value returned from the proc # # @yield the delayed operation to perform # diff --git a/lib/concurrent/synchronization_object_impl/abstract_object.rb b/lib/concurrent/synchronization_object_impl/abstract_object.rb index 9901f3f17..bf5fa8bd3 100644 --- a/lib/concurrent/synchronization_object_impl/abstract_object.rb +++ b/lib/concurrent/synchronization_object_impl/abstract_object.rb @@ -1,20 +1,21 @@ module Concurrent module SynchronizationObjectImpl + # Safe synchronization under any Ruby implementation. # It provides methods like {#synchronize}, {#ns_wait}, {#ns_signal} and {#ns_broadcast}. # Provides a single layer which can improve its implementation over time without changes needed to - # the classes using it. Use {Synchronization::Object} not this abstract class. + # the classes using it. Use {Concurrent::SynchronizationObject} not this abstract class. # # @note this object does not support usage together with # [`Thread#wakeup`](http://ruby-doc.org/core-2.2.0/Thread.html#method-i-wakeup) # and [`Thread#raise`](http://ruby-doc.org/core-2.2.0/Thread.html#method-i-raise). - # `Thread#sleep` and `Thread#wakeup` will work as expected but mixing `Synchronization::Object#wait` and + # `Thread#sleep` and `Thread#wakeup` will work as expected but mixing `Concurrent::SynchronizationObject#wait` and # `Thread#wakeup` will not work on all platforms. # # @see {Event} implementation as an example of this class use # # @example simple - # class AnClass < Synchronization::Object + # class AnClass < SynchronizationObject # def initialize # super # synchronize { @value = 'asd' } From 12cdd7a009fbdf1c7162ded93545963d6dd04d7f Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Thu, 21 May 2015 17:52:34 -0600 Subject: [PATCH 09/11] Moved file map from lib/concurrent to support. --- concurrent-ruby-edge.gemspec | 5 +++-- concurrent-ruby.gemspec | 5 +++-- lib/concurrent/collections.rb | 1 - lib/concurrent/file_map.rb | 18 ------------------ support/file_map.rb | 17 +++++++++++++++++ 5 files changed, 23 insertions(+), 23 deletions(-) delete mode 100644 lib/concurrent/collections.rb delete mode 100644 lib/concurrent/file_map.rb create mode 100644 support/file_map.rb diff --git a/concurrent-ruby-edge.gemspec b/concurrent-ruby-edge.gemspec index a43fe8385..6cc635a85 100644 --- a/concurrent-ruby-edge.gemspec +++ b/concurrent-ruby-edge.gemspec @@ -1,7 +1,8 @@ $:.push File.join(File.dirname(__FILE__), 'lib') +$:.push File.join(File.dirname(__FILE__), 'support') require 'concurrent/version' -require 'concurrent/file_map' +require 'file_map' Gem::Specification.new do |s| git_files = `git ls-files`.split("\n") @@ -15,7 +16,7 @@ Gem::Specification.new do |s| s.summary = 'Edge features and additions to the concurrent-ruby gem.' s.license = 'MIT' s.date = Time.now.strftime('%Y-%m-%d') - s.files = Concurrent::FILE_MAP.fetch :edge + s.files = FileMap::MAP.fetch(:edge) s.extra_rdoc_files = Dir['README*', 'LICENSE*'] s.require_paths = ['lib'] s.description = <<-TXT diff --git a/concurrent-ruby.gemspec b/concurrent-ruby.gemspec index 63c12eac0..bf94d23a6 100644 --- a/concurrent-ruby.gemspec +++ b/concurrent-ruby.gemspec @@ -1,7 +1,8 @@ $:.push File.join(File.dirname(__FILE__), 'lib') +$:.push File.join(File.dirname(__FILE__), 'support') require 'concurrent/version' -require 'concurrent/file_map' +require 'file_map' Gem::Specification.new do |s| git_files = `git ls-files`.split("\n") @@ -15,7 +16,7 @@ Gem::Specification.new do |s| s.summary = 'Modern concurrency tools for Ruby. Inspired by Erlang, Clojure, Scala, Haskell, F#, C#, Java, and classic concurrency patterns.' s.license = 'MIT' s.date = Time.now.strftime('%Y-%m-%d') - s.files = Concurrent::FILE_MAP.fetch :core + s.files = FileMap::MAP.fetch(:core) s.extra_rdoc_files = Dir['README*', 'LICENSE*', 'CHANGELOG*'] s.require_paths = ['lib'] s.description = <<-EOF diff --git a/lib/concurrent/collections.rb b/lib/concurrent/collections.rb deleted file mode 100644 index 8f1e2bb04..000000000 --- a/lib/concurrent/collections.rb +++ /dev/null @@ -1 +0,0 @@ -require 'concurrent/collection/priority_queue' diff --git a/lib/concurrent/file_map.rb b/lib/concurrent/file_map.rb deleted file mode 100644 index e22a96fc1..000000000 --- a/lib/concurrent/file_map.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Concurrent - - git_files = `git ls-files`.split("\n") - all_lib_files = (Dir['lib/concurrent/**/*.rb'] & git_files) - ['lib/concurrent/file_map.rb'] - edge_lib_files = Dir['lib/concurrent/actor.rb', - 'lib/concurrent/actor/**/*.rb', - 'lib/concurrent/channel.rb', - 'lib/concurrent/channel/**/*.rb', - 'lib/concurrent/agent.rb', - 'lib/concurrent/edge/**/*.rb'] & git_files - core_lib_files = all_lib_files - edge_lib_files - - FILE_MAP = { - core: core_lib_files + %w(lib/concurrent.rb lib/concurrent_ruby.rb), - edge: edge_lib_files + %w(lib/concurrent-edge.rb) - } -end - diff --git a/support/file_map.rb b/support/file_map.rb new file mode 100644 index 000000000..ca3b3a98f --- /dev/null +++ b/support/file_map.rb @@ -0,0 +1,17 @@ +module FileMap + + GIT_FILES = `git ls-files`.split("\n") + ALL_LIB_FILES = Dir['lib/concurrent/**/*.rb'] & GIT_FILES + EDGE_LIB_FILES = Dir['lib/concurrent/actor.rb', + 'lib/concurrent/actor/**/*.rb', + 'lib/concurrent/channel.rb', + 'lib/concurrent/channel/**/*.rb', + 'lib/concurrent/agent.rb', + 'lib/concurrent/edge/**/*.rb'] & GIT_FILES + CORE_LIB_FILES = ALL_LIB_FILES - EDGE_LIB_FILES + + MAP = { + core: CORE_LIB_FILES + %w(lib/concurrent.rb lib/concurrent_ruby.rb), + edge: EDGE_LIB_FILES + %w(lib/concurrent-edge.rb) + } +end From 28753f8cab74b78dd4b1e10d0967d04d88517882 Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Thu, 21 May 2015 18:25:15 -0600 Subject: [PATCH 10/11] Moved native extension helpers into Utility module. --- Rakefile | 2 +- ext/concurrent/extconf.rb | 2 +- lib/concurrent/atomic/atomic_boolean.rb | 2 +- lib/concurrent/atomic/atomic_fixnum.rb | 2 +- lib/concurrent/atomic/atomic_reference.rb | 2 +- lib/concurrent/atomic_reference/jruby.rb | 2 +- lib/concurrent/atomic_reference/ruby.rb | 2 +- lib/concurrent/native_extensions.rb | 2 - lib/concurrent/native_extensions/before.rb | 1 - lib/concurrent/native_extensions/load.rb | 46 ---------------- lib/concurrent/synchronization_object.rb | 2 +- .../utility/native_extension_loader.rb | 54 +++++++++++++++++++ spec/support/example_group_extensions.rb | 3 +- 13 files changed, 64 insertions(+), 58 deletions(-) delete mode 100644 lib/concurrent/native_extensions.rb delete mode 100644 lib/concurrent/native_extensions/before.rb delete mode 100644 lib/concurrent/native_extensions/load.rb create mode 100644 lib/concurrent/utility/native_extension_loader.rb diff --git a/Rakefile b/Rakefile index f1a0cf826..998c9fdf8 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,7 @@ #!/usr/bin/env rake require 'concurrent/version' -require 'concurrent/native_extensions' +require 'concurrent/utility/native_extension_loader' ## load the two gemspec files CORE_GEMSPEC = Gem::Specification.load('concurrent-ruby.gemspec') diff --git a/ext/concurrent/extconf.rb b/ext/concurrent/extconf.rb index 716b114a4..5775025c6 100644 --- a/ext/concurrent/extconf.rb +++ b/ext/concurrent/extconf.rb @@ -1,6 +1,6 @@ require 'fileutils' -require 'concurrent/native_extensions' +require 'concurrent/utility/native_extension_loader' EXTENSION_NAME = 'extension' diff --git a/lib/concurrent/atomic/atomic_boolean.rb b/lib/concurrent/atomic/atomic_boolean.rb index 76d7c96f2..c0a39812b 100644 --- a/lib/concurrent/atomic/atomic_boolean.rb +++ b/lib/concurrent/atomic/atomic_boolean.rb @@ -1,5 +1,5 @@ -require 'concurrent/native_extensions' require 'concurrent/synchronization_object' +require 'concurrent/utility/native_extension_loader' module Concurrent diff --git a/lib/concurrent/atomic/atomic_fixnum.rb b/lib/concurrent/atomic/atomic_fixnum.rb index 333dc0148..b2df8060e 100644 --- a/lib/concurrent/atomic/atomic_fixnum.rb +++ b/lib/concurrent/atomic/atomic_fixnum.rb @@ -1,5 +1,5 @@ -require 'concurrent/native_extensions' require 'concurrent/synchronization_object' +require 'concurrent/utility/native_extension_loader' module Concurrent diff --git a/lib/concurrent/atomic/atomic_reference.rb b/lib/concurrent/atomic/atomic_reference.rb index 6450d846b..c39f143f0 100644 --- a/lib/concurrent/atomic/atomic_reference.rb +++ b/lib/concurrent/atomic/atomic_reference.rb @@ -1,5 +1,5 @@ -require 'concurrent/native_extensions' require 'concurrent/utility/engine' +require 'concurrent/utility/native_extension_loader' require 'concurrent/atomic_reference/concurrent_update_error' require 'concurrent/atomic_reference/mutex_atomic' diff --git a/lib/concurrent/atomic_reference/jruby.rb b/lib/concurrent/atomic_reference/jruby.rb index c22cd34b2..5916fdbfe 100644 --- a/lib/concurrent/atomic_reference/jruby.rb +++ b/lib/concurrent/atomic_reference/jruby.rb @@ -1,4 +1,4 @@ -require 'concurrent/native_extensions' +require 'concurrent/utility/native_extension_loader' if defined?(Concurrent::JavaAtomicReference) require 'concurrent/atomic_reference/direct_update' diff --git a/lib/concurrent/atomic_reference/ruby.rb b/lib/concurrent/atomic_reference/ruby.rb index 14c51a188..affe4de69 100644 --- a/lib/concurrent/atomic_reference/ruby.rb +++ b/lib/concurrent/atomic_reference/ruby.rb @@ -1,7 +1,7 @@ if defined? Concurrent::CAtomicReference - require 'concurrent/native_extensions' require 'concurrent/atomic_reference/direct_update' require 'concurrent/atomic_reference/numeric_cas_wrapper' + require 'concurrent/utility/native_extension_loader' module Concurrent diff --git a/lib/concurrent/native_extensions.rb b/lib/concurrent/native_extensions.rb deleted file mode 100644 index 8095aeb93..000000000 --- a/lib/concurrent/native_extensions.rb +++ /dev/null @@ -1,2 +0,0 @@ -require 'concurrent/native_extensions/before' -require 'concurrent/native_extensions/load' diff --git a/lib/concurrent/native_extensions/before.rb b/lib/concurrent/native_extensions/before.rb deleted file mode 100644 index 79937938a..000000000 --- a/lib/concurrent/native_extensions/before.rb +++ /dev/null @@ -1 +0,0 @@ -require 'concurrent/synchronization_object_impl/abstract_object' diff --git a/lib/concurrent/native_extensions/load.rb b/lib/concurrent/native_extensions/load.rb deleted file mode 100644 index 0ccc7ec70..000000000 --- a/lib/concurrent/native_extensions/load.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'concurrent/utility/engine' - -module Concurrent - - @c_ext_loaded ||= false - @java_ext_loaded ||= false - - # @!visibility private - def self.allow_c_extensions? - on_cruby? - end - - if allow_c_extensions? && !@c_ext_loaded - tries = [ - lambda do - require 'concurrent/extension' - @c_ext_loaded = true - end, - lambda do - # may be a Windows cross-compiled native gem - require "concurrent/#{RUBY_VERSION[0..2]}/extension" - @c_ext_loaded = true - end, - lambda do - warn 'Performance on MRI may be improved with the concurrent-ruby-ext gem. Please see http://concurrent-ruby.com' - end] - - tries.each do |try| - begin - try.call - break - rescue LoadError - next - end - end - end - - if on_jruby? && !@java_ext_loaded - begin - require 'concurrent_ruby_ext' - @java_ext_loaded = true - rescue LoadError - warn 'Performance on JRuby may be improved by installing the pre-compiled Java extensions. Please see http://concurrent-ruby.com' - end - end -end diff --git a/lib/concurrent/synchronization_object.rb b/lib/concurrent/synchronization_object.rb index 1fdaa3086..4a906302f 100644 --- a/lib/concurrent/synchronization_object.rb +++ b/lib/concurrent/synchronization_object.rb @@ -1,5 +1,5 @@ require 'concurrent/utility/engine' -require 'concurrent/native_extensions' # JavaObject +require 'concurrent/utility/native_extension_loader' require 'concurrent/synchronization_object_impl/abstract_object' require 'concurrent/synchronization_object_impl/mutex_object' require 'concurrent/synchronization_object_impl/monitor_object' diff --git a/lib/concurrent/utility/native_extension_loader.rb b/lib/concurrent/utility/native_extension_loader.rb new file mode 100644 index 000000000..6637214ce --- /dev/null +++ b/lib/concurrent/utility/native_extension_loader.rb @@ -0,0 +1,54 @@ +require 'concurrent/synchronization_object_impl/abstract_object' # for JRuby +require 'concurrent/utility/engine' + +module Concurrent + module Utility + + module NativeExtensionLoader + + @c_ext_loaded ||= false + @java_ext_loaded ||= false + + # @!visibility private + def allow_c_extensions? + Concurrent.on_cruby? + end + + if Concurrent.on_cruby? && !@c_ext_loaded + tries = [ + lambda do + require 'concurrent/extension' + @c_ext_loaded = true + end, + lambda do + # may be a Windows cross-compiled native gem + require "concurrent/#{RUBY_VERSION[0..2]}/extension" + @c_ext_loaded = true + end, + lambda do + warn 'Performance on MRI may be improved with the concurrent-ruby-ext gem. Please see http://concurrent-ruby.com' + end] + + tries.each do |try| + begin + try.call + break + rescue LoadError + next + end + end + end + + if Concurrent.on_jruby? && !@java_ext_loaded + begin + require 'concurrent_ruby_ext' + @java_ext_loaded = true + rescue LoadError + warn 'Performance on JRuby may be improved by installing the pre-compiled Java extensions. Please see http://concurrent-ruby.com' + end + end + end + end + + extend Utility::NativeExtensionLoader +end diff --git a/spec/support/example_group_extensions.rb b/spec/support/example_group_extensions.rb index 15edb49fa..afffd57c7 100644 --- a/spec/support/example_group_extensions.rb +++ b/spec/support/example_group_extensions.rb @@ -1,6 +1,6 @@ require 'rbconfig' -require 'concurrent/native_extensions' require 'concurrent/utility/engine' +require 'concurrent/utility/native_extension_loader' module Concurrent module TestHelpers @@ -15,6 +15,7 @@ def delta(v1, v2) end include Utility::EngineDetector + include Utility::NativeExtensionLoader def use_c_extensions? Concurrent.allow_c_extensions? # from extension_helper.rb From 3c14f4c700e03e1f664089408efcb492c1b20047 Mon Sep 17 00:00:00 2001 From: Jerry D'Antonio Date: Fri, 22 May 2015 18:36:56 -0600 Subject: [PATCH 11/11] Moved structs to root level. --- README.md | 37 ++--- lib/concurrent.rb | 4 +- lib/concurrent/collection/abstract_struct.rb | 150 ++++++++++++++++++ .../{struct => }/immutable_struct.rb | 6 +- lib/concurrent/{struct => }/mutable_struct.rb | 8 +- .../{struct => }/settable_struct.rb | 6 +- lib/concurrent/struct.rb | 3 - lib/concurrent/struct/abstract_struct.rb | 148 ----------------- .../{struct => }/immutable_struct_spec.rb | 0 .../{struct => }/mutable_struct_spec.rb | 0 .../{struct => }/settable_struct_spec.rb | 0 spec/concurrent/{struct => }/struct_shared.rb | 0 12 files changed, 182 insertions(+), 180 deletions(-) create mode 100644 lib/concurrent/collection/abstract_struct.rb rename lib/concurrent/{struct => }/immutable_struct.rb (90%) rename lib/concurrent/{struct => }/mutable_struct.rb (96%) rename lib/concurrent/{struct => }/settable_struct.rb (94%) delete mode 100644 lib/concurrent/struct.rb delete mode 100644 lib/concurrent/struct/abstract_struct.rb rename spec/concurrent/{struct => }/immutable_struct_spec.rb (100%) rename spec/concurrent/{struct => }/mutable_struct_spec.rb (100%) rename spec/concurrent/{struct => }/settable_struct_spec.rb (100%) rename spec/concurrent/{struct => }/struct_shared.rb (100%) diff --git a/README.md b/README.md index 6e4dc2371..63acf334c 100644 --- a/README.md +++ b/README.md @@ -121,31 +121,32 @@ require 'concurrent' # everything # groups -require 'concurrent/atomics' # atomic and thread synchronization classes +require 'concurrent/atomics' # Atomic and thread synchronization classes require 'concurrent/executors' # Thread pools and other executors -require 'concurrent/utilities' # utility methods such as processor count and timers # individual abstractions -require 'concurrent/agent' # Concurrent::Agent -require 'concurrent/async' # Concurrent::Async -require 'concurrent/atomic' # Concurrent::Atomic (formerly the `atomic` gem) -require 'concurrent/dataflow' # Concurrent::dataflow -require 'concurrent/delay' # Concurrent::Delay -require 'concurrent/exchanger' # Concurrent::Exchanger -require 'concurrent/future' # Concurrent::Future -require 'concurrent/ivar' # Concurrent::IVar -require 'concurrent/lazy_register' # Concurrent::LazyRegister -require 'concurrent/mvar' # Concurrent::MVar -require 'concurrent/promise' # Concurrent::Promise -require 'concurrent/scheduled_task' # Concurrent::ScheduledTask -require 'concurrent/timer_task' # Concurrent::TimerTask -require 'concurrent/tvar' # Concurrent::TVar +require 'concurrent/async' # Concurrent::Async +require 'concurrent/dataflow' # Concurrent::dataflow +require 'concurrent/delay' # Concurrent::Delay +require 'concurrent/future' # Concurrent::Future +require 'concurrent/immutable_struct' # Concurrent::ImmutableStruct +require 'concurrent/ivar' # Concurrent::IVar +require 'concurrent/mutable_struct' # Concurrent::MutableStruct +require 'concurrent/mvar' # Concurrent::MVar +require 'concurrent/promise' # Concurrent::Promise +require 'concurrent/scheduled_task' # Concurrent::ScheduledTask +require 'concurrent/settable_struct' # Concurrent::SettableStruct +require 'concurrent/timer_task' # Concurrent::TimerTask +require 'concurrent/tvar' # Concurrent::TVar # experimental - available in `concurrent-ruby-edge` companion gem -require 'concurrent/actor' # Concurrent::Actor and supporting code -require 'concurrent/channel ' # Concurrent::Channel and supporting code +require 'concurrent/agent' # Concurrent::Agent +require 'concurrent/actor' # Concurrent::Actor and supporting code +require 'concurrent/channel ' # Concurrent::Channel and supporting code +require 'concurrent/edge/exchanger' # Concurrent::Edge::Exchanger +require 'concurrent/edge/lazy_register' # Concurrent::Edge::LazyRegister ``` ## Installation diff --git a/lib/concurrent.rb b/lib/concurrent.rb index 8c1048f7a..074ca1107 100644 --- a/lib/concurrent.rb +++ b/lib/concurrent.rb @@ -5,17 +5,19 @@ require 'concurrent/atomics' require 'concurrent/errors' require 'concurrent/executors' -require 'concurrent/struct' require 'concurrent/atomic/atomic_reference' require 'concurrent/async' require 'concurrent/dataflow' require 'concurrent/delay' require 'concurrent/future' +require 'concurrent/immutable_struct' require 'concurrent/ivar' +require 'concurrent/mutable_struct' require 'concurrent/mvar' require 'concurrent/promise' require 'concurrent/scheduled_task' +require 'concurrent/settable_struct' require 'concurrent/synchronization_object' require 'concurrent/timer_task' require 'concurrent/tvar' diff --git a/lib/concurrent/collection/abstract_struct.rb b/lib/concurrent/collection/abstract_struct.rb new file mode 100644 index 000000000..5a434cb23 --- /dev/null +++ b/lib/concurrent/collection/abstract_struct.rb @@ -0,0 +1,150 @@ +module Concurrent + module Collection + + # @!visibility private + module AbstractStruct + + # @!macro [attach] struct_length + # + # Returns the number of struct members. + # + # @return [Fixnum] the number of struct members + def length + self.class::MEMBERS.length + end + alias_method :size, :length + + # @!macro [attach] struct_members + # + # Returns the struct members as an array of symbols. + # + # @return [Array] the struct members as an array of symbols + def members + self.class::MEMBERS.dup + end + + protected + + # @!macro struct_values + # + # @!visibility private + def ns_values + @values.dup + end + + # @!macro struct_values_at + # + # @!visibility private + def ns_values_at(indexes) + @values.values_at(*indexes) + end + + # @!macro struct_to_h + # + # @!visibility private + def ns_to_h + length.times.reduce({}){|memo, i| memo[self.class::MEMBERS[i]] = @values[i]; memo} + end + + # @!macro struct_get + # + # @!visibility private + def ns_get(member) + if member.is_a? Integer + if member >= @values.length + raise IndexError.new("offset #{member} too large for struct(size:#{@values.length})") + end + @values[member] + else + send(member) + end + rescue NoMethodError + raise NameError.new("no member '#{member}' in struct") + end + + # @!macro struct_equality + # + # @!visibility private + def ns_equality(other) + self.class == other.class && self.values == other.values + end + + # @!macro struct_each + # + # @!visibility private + def ns_each + values.each{|value| yield value } + end + + # @!macro struct_each_pair + # + # @!visibility private + def ns_each_pair + @values.length.times do |index| + yield self.class::MEMBERS[index], @values[index] + end + end + + # @!macro struct_select + # + # @!visibility private + def ns_select + values.select{|value| yield value } + end + + # @!macro struct_inspect + # + # @!visibility private + def ns_inspect + struct = pr_underscore(self.class.ancestors[1]) + clazz = ((self.class.to_s =~ /^#" + end + + # @!macro struct_merge + # + # @!visibility private + def ns_merge(other, &block) + self.class.new(*self.to_h.merge(other, &block).values) + end + + # @!visibility private + def pr_underscore(clazz) + word = clazz.to_s + word.gsub!(/::/, '/') + word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') + word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') + word.tr!("-", "_") + word.downcase! + word + end + + # @!visibility private + def self.define_struct_class(parent, base, name, members, &block) + clazz = Class.new(base || Object) do + include parent + self.const_set(:MEMBERS, members.collect{|member| member.to_s.to_sym}.freeze) + def ns_initialize(*values) + raise ArgumentError.new('struct size differs') if values.length > length + @values = values.fill(nil, values.length..length-1) + end + end + unless name.nil? + begin + parent.const_set(name, clazz) + parent.const_get(name) + rescue NameError + raise NameError.new("identifier #{name} needs to be constant") + end + end + members.each_with_index do |member, index| + clazz.send(:define_method, member) do + @values[index] + end + end + clazz.class_exec(&block) unless block.nil? + clazz + end + end + end +end diff --git a/lib/concurrent/struct/immutable_struct.rb b/lib/concurrent/immutable_struct.rb similarity index 90% rename from lib/concurrent/struct/immutable_struct.rb rename to lib/concurrent/immutable_struct.rb index a20c4900e..11a23e94c 100644 --- a/lib/concurrent/struct/immutable_struct.rb +++ b/lib/concurrent/immutable_struct.rb @@ -1,4 +1,4 @@ -require 'concurrent/struct/abstract_struct' +require 'concurrent/collection/abstract_struct' require 'concurrent/synchronization_object' module Concurrent @@ -7,7 +7,7 @@ module Concurrent # # @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct` module ImmutableStruct - include AbstractStruct + include Collection::AbstractStruct # @!visibility private def initialize(*values) @@ -83,7 +83,7 @@ def self.new(*args, &block) FACTORY = Class.new(SynchronizationObject) do def define_struct(name, members, &block) synchronize do - AbstractStruct.define_struct_class(ImmutableStruct, nil, name, members, &block) + Collection::AbstractStruct.define_struct_class(ImmutableStruct, nil, name, members, &block) end end end.new diff --git a/lib/concurrent/struct/mutable_struct.rb b/lib/concurrent/mutable_struct.rb similarity index 96% rename from lib/concurrent/struct/mutable_struct.rb rename to lib/concurrent/mutable_struct.rb index 4509fa902..d782e412c 100644 --- a/lib/concurrent/struct/mutable_struct.rb +++ b/lib/concurrent/mutable_struct.rb @@ -1,4 +1,4 @@ -require 'concurrent/struct/abstract_struct' +require 'concurrent/collection/abstract_struct' require 'concurrent/synchronization_object' module Concurrent @@ -8,7 +8,7 @@ module Concurrent # # @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct` module MutableStruct - include AbstractStruct + include Collection::AbstractStruct # @!macro [new] struct_new # @@ -88,7 +88,7 @@ def inspect # @yieldparam [Object] selfvalue the value of the member in `self` # @yieldparam [Object] othervalue the value of the member in `other` # - # @return [AbstractStruct] a new struct with the new values + # @return [Collection::AbstractStruct] a new struct with the new values # # @raise [ArgumentError] of given a member that is not defined in the struct def merge(other, &block) @@ -209,7 +209,7 @@ def self.new(*args, &block) FACTORY = Class.new(SynchronizationObject) do def define_struct(name, members, &block) synchronize do - clazz = AbstractStruct.define_struct_class(MutableStruct, SynchronizationObject, name, members, &block) + clazz = Collection::AbstractStruct.define_struct_class(MutableStruct, SynchronizationObject, name, members, &block) members.each_with_index do |member, index| clazz.send(:define_method, member) do synchronize { @values[index] } diff --git a/lib/concurrent/struct/settable_struct.rb b/lib/concurrent/settable_struct.rb similarity index 94% rename from lib/concurrent/struct/settable_struct.rb rename to lib/concurrent/settable_struct.rb index c6e7f0398..8fac604ee 100644 --- a/lib/concurrent/struct/settable_struct.rb +++ b/lib/concurrent/settable_struct.rb @@ -1,4 +1,4 @@ -require 'concurrent/struct/abstract_struct' +require 'concurrent/collection/abstract_struct' require 'concurrent/errors' require 'concurrent/synchronization_object' @@ -12,7 +12,7 @@ module Concurrent # @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct` # @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword module SettableStruct - include AbstractStruct + include Collection::AbstractStruct # @!macro struct_values def values @@ -104,7 +104,7 @@ def self.new(*args, &block) FACTORY = Class.new(SynchronizationObject) do def define_struct(name, members, &block) synchronize do - clazz = AbstractStruct.define_struct_class(SettableStruct, SynchronizationObject, name, members, &block) + clazz = Collection::AbstractStruct.define_struct_class(SettableStruct, SynchronizationObject, name, members, &block) members.each_with_index do |member, index| clazz.send(:define_method, member) do synchronize { @values[index] } diff --git a/lib/concurrent/struct.rb b/lib/concurrent/struct.rb deleted file mode 100644 index 6aacba684..000000000 --- a/lib/concurrent/struct.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'concurrent/struct/immutable_struct' -require 'concurrent/struct/mutable_struct' -require 'concurrent/struct/settable_struct' diff --git a/lib/concurrent/struct/abstract_struct.rb b/lib/concurrent/struct/abstract_struct.rb deleted file mode 100644 index 443c0664f..000000000 --- a/lib/concurrent/struct/abstract_struct.rb +++ /dev/null @@ -1,148 +0,0 @@ -module Concurrent - - # @!visibility private - module AbstractStruct - - # @!macro [attach] struct_length - # - # Returns the number of struct members. - # - # @return [Fixnum] the number of struct members - def length - self.class::MEMBERS.length - end - alias_method :size, :length - - # @!macro [attach] struct_members - # - # Returns the struct members as an array of symbols. - # - # @return [Array] the struct members as an array of symbols - def members - self.class::MEMBERS.dup - end - - protected - - # @!macro struct_values - # - # @!visibility private - def ns_values - @values.dup - end - - # @!macro struct_values_at - # - # @!visibility private - def ns_values_at(indexes) - @values.values_at(*indexes) - end - - # @!macro struct_to_h - # - # @!visibility private - def ns_to_h - length.times.reduce({}){|memo, i| memo[self.class::MEMBERS[i]] = @values[i]; memo} - end - - # @!macro struct_get - # - # @!visibility private - def ns_get(member) - if member.is_a? Integer - if member >= @values.length - raise IndexError.new("offset #{member} too large for struct(size:#{@values.length})") - end - @values[member] - else - send(member) - end - rescue NoMethodError - raise NameError.new("no member '#{member}' in struct") - end - - # @!macro struct_equality - # - # @!visibility private - def ns_equality(other) - self.class == other.class && self.values == other.values - end - - # @!macro struct_each - # - # @!visibility private - def ns_each - values.each{|value| yield value } - end - - # @!macro struct_each_pair - # - # @!visibility private - def ns_each_pair - @values.length.times do |index| - yield self.class::MEMBERS[index], @values[index] - end - end - - # @!macro struct_select - # - # @!visibility private - def ns_select - values.select{|value| yield value } - end - - # @!macro struct_inspect - # - # @!visibility private - def ns_inspect - struct = pr_underscore(self.class.ancestors[1]) - clazz = ((self.class.to_s =~ /^#" - end - - # @!macro struct_merge - # - # @!visibility private - def ns_merge(other, &block) - self.class.new(*self.to_h.merge(other, &block).values) - end - - # @!visibility private - def pr_underscore(clazz) - word = clazz.to_s - word.gsub!(/::/, '/') - word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') - word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') - word.tr!("-", "_") - word.downcase! - word - end - - # @!visibility private - def self.define_struct_class(parent, base, name, members, &block) - clazz = Class.new(base || Object) do - include parent - self.const_set(:MEMBERS, members.collect{|member| member.to_s.to_sym}.freeze) - def ns_initialize(*values) - raise ArgumentError.new('struct size differs') if values.length > length - @values = values.fill(nil, values.length..length-1) - end - end - unless name.nil? - begin - parent.const_set(name, clazz) - parent.const_get(name) - rescue NameError - raise NameError.new("identifier #{name} needs to be constant") - end - end - members.each_with_index do |member, index| - clazz.send(:define_method, member) do - @values[index] - end - end - clazz.class_exec(&block) unless block.nil? - clazz - end - end -end diff --git a/spec/concurrent/struct/immutable_struct_spec.rb b/spec/concurrent/immutable_struct_spec.rb similarity index 100% rename from spec/concurrent/struct/immutable_struct_spec.rb rename to spec/concurrent/immutable_struct_spec.rb diff --git a/spec/concurrent/struct/mutable_struct_spec.rb b/spec/concurrent/mutable_struct_spec.rb similarity index 100% rename from spec/concurrent/struct/mutable_struct_spec.rb rename to spec/concurrent/mutable_struct_spec.rb diff --git a/spec/concurrent/struct/settable_struct_spec.rb b/spec/concurrent/settable_struct_spec.rb similarity index 100% rename from spec/concurrent/struct/settable_struct_spec.rb rename to spec/concurrent/settable_struct_spec.rb diff --git a/spec/concurrent/struct/struct_shared.rb b/spec/concurrent/struct_shared.rb similarity index 100% rename from spec/concurrent/struct/struct_shared.rb rename to spec/concurrent/struct_shared.rb