diff --git a/lib/concurrent/array.rb b/lib/concurrent/array.rb index c83f3bb06..f3786056e 100644 --- a/lib/concurrent/array.rb +++ b/lib/concurrent/array.rb @@ -2,7 +2,8 @@ require 'concurrent/thread_safe/util' module Concurrent - if Concurrent.on_cruby? + case + when Concurrent.on_cruby? # Because MRI never runs code in parallel, the existing # non-thread-safe structures should usually work fine. @@ -21,10 +22,10 @@ module Concurrent # may be lost. Use `#concat` instead. # # @see http://ruby-doc.org/core-2.2.0/Array.html Ruby standard library `Array` - class Array < ::Array; + class Array < ::Array end - elsif Concurrent.on_jruby? + when Concurrent.on_jruby? require 'jruby/synchronized' # @!macro concurrent_array @@ -32,15 +33,29 @@ class Array < ::Array include JRuby::Synchronized end - elsif Concurrent.on_rbx? || Concurrent.on_truffleruby? + when Concurrent.on_rbx? require 'monitor' - require 'concurrent/thread_safe/util/array_hash_rbx' + require 'concurrent/thread_safe/util/data_structures' # @!macro concurrent_array class Array < ::Array end ThreadSafe::Util.make_synchronized_on_rbx Concurrent::Array + + when Concurrent.on_truffleruby? + require 'concurrent/thread_safe/util/data_structures' + + # @!macro concurrent_array + class Array < ::Array + end + + ThreadSafe::Util.make_synchronized_on_truffleruby Concurrent::Array + + else + warn 'Possibly unsupported Ruby implementation' + class Array < ::Array + end end end diff --git a/lib/concurrent/hash.rb b/lib/concurrent/hash.rb index 9c952c1fb..3ecc2ff04 100644 --- a/lib/concurrent/hash.rb +++ b/lib/concurrent/hash.rb @@ -2,7 +2,8 @@ require 'concurrent/thread_safe/util' module Concurrent - if Concurrent.on_cruby? + case + when Concurrent.on_cruby? # @!macro [attach] concurrent_hash # @@ -12,10 +13,10 @@ module Concurrent # which takes the lock repeatedly when reading an item. # # @see http://ruby-doc.org/core-2.2.0/Hash.html Ruby standard library `Hash` - class Hash < ::Hash; + class Hash < ::Hash end - elsif Concurrent.on_jruby? + when Concurrent.on_jruby? require 'jruby/synchronized' # @!macro concurrent_hash @@ -23,14 +24,30 @@ class Hash < ::Hash include JRuby::Synchronized end - elsif Concurrent.on_rbx? || Concurrent.on_truffleruby? + when Concurrent.on_rbx? require 'monitor' - require 'concurrent/thread_safe/util/array_hash_rbx' + require 'concurrent/thread_safe/util/data_structures' # @!macro concurrent_hash class Hash < ::Hash end ThreadSafe::Util.make_synchronized_on_rbx Concurrent::Hash + + when Concurrent.on_truffleruby? + require 'concurrent/thread_safe/util/data_structures' + + # @!macro concurrent_hash + class Hash < ::Hash + end + + ThreadSafe::Util.make_synchronized_on_truffleruby Concurrent::Hash + + else + warn 'Possibly unsupported Ruby implementation' + class Hash < ::Hash + end + end end + diff --git a/lib/concurrent/set.rb b/lib/concurrent/set.rb index 7e9956f29..8e72d9bb4 100644 --- a/lib/concurrent/set.rb +++ b/lib/concurrent/set.rb @@ -3,7 +3,8 @@ require 'set' module Concurrent - if Concurrent.on_cruby? + case + when Concurrent.on_cruby? # Because MRI never runs code in parallel, the existing # non-thread-safe structures should usually work fine. @@ -25,7 +26,7 @@ module Concurrent class Set < ::Set; end - elsif Concurrent.on_jruby? + when Concurrent.on_jruby? require 'jruby/synchronized' # @!macro concurrent_Set @@ -33,15 +34,29 @@ class Set < ::Set include JRuby::Synchronized end - elsif Concurrent.on_rbx? || Concurrent.on_truffleruby? + when Concurrent.on_rbx? require 'monitor' - require 'concurrent/thread_safe/util/array_hash_rbx' + require 'concurrent/thread_safe/util/data_structures' # @!macro concurrent_Set class Set < ::Set end - ThreadSafe::Util.make_synchronized_on_rbx Set + ThreadSafe::Util.make_synchronized_on_rbx Concurrent::Set + + when Concurrent.on_truffleruby? + require 'concurrent/thread_safe/util/data_structures' + + # @!macro concurrent_array + class Set < ::Set + end + + ThreadSafe::Util.make_synchronized_on_truffleruby Concurrent::Set + + else + warn 'Possibly unsupported Ruby implementation' + class Set < ::Set + end end end diff --git a/lib/concurrent/thread_safe/util/array_hash_rbx.rb b/lib/concurrent/thread_safe/util/data_structures.rb similarity index 56% rename from lib/concurrent/thread_safe/util/array_hash_rbx.rb rename to lib/concurrent/thread_safe/util/data_structures.rb index 2ee8ce748..e43d9c50a 100644 --- a/lib/concurrent/thread_safe/util/array_hash_rbx.rb +++ b/lib/concurrent/thread_safe/util/data_structures.rb @@ -6,12 +6,13 @@ module Util def self.make_synchronized_on_rbx(klass) klass.class_eval do private + def _mon_initialize @_monitor = Monitor.new unless @_monitor # avoid double initialisation end - def self.new - obj = super + def self.new(*args) + obj = super(*args) obj.send(:_mon_initialize) obj end @@ -30,12 +31,25 @@ def #{method}(*args) else klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args) - @_monitor.synchronize { super } + monitor = @_monitor + monitor or raise("BUG: Internal monitor was not properly initialized. Please report this to the concurrent-ruby developers.") + monitor.synchronize { super } end RUBY end end end + + def self.make_synchronized_on_truffleruby(klass) + klass.superclass.instance_methods(false).each do |method| + klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + # TODO (pitr-ch 01-Jul-2018): don't use internal TruffleRuby APIs + Truffle::System.synchronized(self) { super(*args, &block) } + end + RUBY + end + end end end end diff --git a/spec/concurrent/array_spec.rb b/spec/concurrent/array_spec.rb index 83450af89..f79f8e077 100644 --- a/spec/concurrent/array_spec.rb +++ b/spec/concurrent/array_spec.rb @@ -2,22 +2,87 @@ module Concurrent RSpec.describe Array do let!(:ary) { described_class.new } - it 'concurrency' do - (1..Concurrent::ThreadSafe::Test::THREADS).map do |i| - in_thread do - 1000.times do - ary << i - ary.each { |x| x * 2 } - ary.shift - ary.last + describe '.[]' do + describe 'when initializing with no arguments' do + it do + expect(described_class[]).to be_empty + end + end + + describe 'when initializing with arguments' do + it 'creates an array with the given objects' do + expect(described_class[:hello, :world]).to eq [:hello, :world] + end + end + end + + describe '.new' do + describe 'when initializing with no arguments' do + it do + expect(described_class.new).to be_empty + end + end + + describe 'when initializing with a size argument' do + let(:size) { 3 } + + it 'creates an array with size elements set to nil' do + expect(described_class.new(size)).to eq [nil, nil, nil] + end + + describe 'when initializing with a default value argument' do + let(:default_value) { :ruby } + + it 'creates an array with size elements set to the default value' do + expect(described_class.new(size, default_value)).to eq [:ruby, :ruby, :ruby] end end - end.map(&:join) - expect(ary).to be_empty + + describe 'when initializing with a block argument' do + let(:block_argument) { proc { |index| :"ruby#{index}" } } + + it 'creates an array with size elements set to the default value' do + expect(described_class.new(size, &block_argument)).to eq [:ruby0, :ruby1, :ruby2] + end + end + end + + describe 'when initializing with another array as an argument' do + let(:other_array) { [:hello, :world] } + let(:fake_other_array) { double('Fake array', to_ary: other_array) } + + it 'creates a new array' do + expect(described_class.new(other_array)).to_not be other_array + end + + it 'creates an array with the same contents as the other array' do + expect(described_class.new(other_array)).to eq [:hello, :world] + end + + it 'creates an array with the results of calling #to_ary on the other array' do + expect(described_class.new(fake_other_array)).to eq [:hello, :world] + end + end + end + + context 'concurrency' do + it do + (1..Concurrent::ThreadSafe::Test::THREADS).map do |i| + in_thread(ary) do |ary| + 1000.times do + ary << i + ary.each { |x| x * 2 } + ary.shift + ary.last + end + end + end.map(&:join) + expect(ary).to be_empty + end end describe '#slice' do - # This is mostly relevant on Rubinius and Truffle + # This is mostly relevant on Rubinius and TruffleRuby it 'correctly initializes the monitor' do ary.concat([0, 1, 2, 3, 4, 5, 6, 7, 8]) diff --git a/spec/concurrent/hash_spec.rb b/spec/concurrent/hash_spec.rb index 37146048e..448d214f8 100644 --- a/spec/concurrent/hash_spec.rb +++ b/spec/concurrent/hash_spec.rb @@ -2,17 +2,103 @@ module Concurrent RSpec.describe Hash do let!(:hsh) { described_class.new } - it 'concurrency' do - (1..Concurrent::ThreadSafe::Test::THREADS).map do |i| - in_thread do - 1000.times do |j| - hsh[i * 1000 + j] = i - expect(hsh[i * 1000 + j]).to eq(i) - expect(hsh.delete(i * 1000 + j)).to eq(i) + describe '.[]' do + describe 'when initializing with no arguments' do + it do + expect(described_class[]).to be_empty + end + end + + describe 'when initializing with an even number of arguments' do + it 'creates a hash using the odd position arguments as keys and even position arguments as values' do + expect(described_class[:hello, 'hello', :world, 'world']).to eq(hello: 'hello', world: 'world') + end + end + + describe 'when initializing with an array of pairs' do + let(:array_of_pairs) { [[:hello, 'hello'], [:world, 'world']] } + + it 'creates a hash using each pair as a (key, value) pair' do + expect(described_class[array_of_pairs]).to eq(hello: 'hello', world: 'world') + end + end + + describe 'when initializing with another hash as an argument' do + let(:other_hash) { {hello: 'hello', world: 'world'} } + let(:fake_other_hash) { double('Fake hash', to_hash: other_hash) } + + it 'creates a new hash' do + expect(described_class[other_hash]).to_not be other_hash + end + + it 'creates a hash with the same contents as the other hash' do + expect(described_class[other_hash]).to eq(hello: 'hello', world: 'world') + end + + it 'creates a hash with the results of calling #to_hash on the other array' do + expect(described_class[fake_other_hash]).to eq(hello: 'hello', world: 'world') + end + end + end + + describe '.new' do + describe 'when initializing with no arguments' do + it do + expect(described_class.new).to be_empty + end + end + + describe 'when initialized with a default object' do + let(:default_object) { :ruby } + + it 'uses the default object for non-existing keys' do + hash = described_class.new(default_object) + + expect(hash[:hello]).to be :ruby + expect(hash[:world]).to be :ruby + end + end + + describe 'when initialized with a block' do + it 'calls the block for non-existing keys' do + block_calls = [] + + hash = described_class.new do |hash_instance, key| + block_calls << [hash_instance, key] + end + + hash[:hello] + hash[:world] + + expect(block_calls).to eq [[hash, :hello], [hash, :world]] + end + + it 'returns the results of calling the block for non-existing key' do + block_results = ['hello', 'world'] + + hash = described_class.new do + block_results.shift end + + expect(hash[:hello]).to eq 'hello' + expect(hash[:world]).to eq 'world' end - end.map(&:join) - expect(hsh).to be_empty + end + end + + context 'concurrency' do + it do + (1..Concurrent::ThreadSafe::Test::THREADS).map do |i| + in_thread do + 1000.times do |j| + hsh[i * 1000 + j] = i + expect(hsh[i * 1000 + j]).to eq(i) + expect(hsh.delete(i * 1000 + j)).to eq(i) + end + end + end.map(&:join) + expect(hsh).to be_empty + end end end end diff --git a/spec/concurrent/set_spec.rb b/spec/concurrent/set_spec.rb index 4b2ba36dd..7c69b4bec 100644 --- a/spec/concurrent/set_spec.rb +++ b/spec/concurrent/set_spec.rb @@ -3,18 +3,58 @@ module Concurrent RSpec.describe Set do let!(:set) { described_class.new } - it 'concurrency' do - (1..Concurrent::ThreadSafe::Test::THREADS).map do |i| - in_thread do - 1000.times do - v = i - set << v - expect(set).not_to be_empty - set.delete(v) + describe '.[]' do + describe 'when initializing with no arguments' do + it do + expect(described_class[]).to be_empty + end + end + + describe 'when initializing with arguments' do + it 'creates a set with the given objects' do + expect(described_class[:hello, :world]).to eq ::Set.new([:hello, :world]) + end + end + end + + describe '.new' do + describe 'when initializing with no arguments' do + it do + expect(described_class.new).to be_empty + end + end + + describe 'when initializing with an enumerable object' do + let(:enumerable_object) { [:hello, :world] } + + it 'creates a set with the contents of the enumerable object' do + expect(described_class.new(enumerable_object)).to eq ::Set.new([:hello, :world]) + end + + describe 'when initializing with a block argument' do + let(:block_argument) { proc { |value| :"#{value}_ruby" } } + + it 'creates a set with the contents of the enumerable object' do + expect(described_class.new(enumerable_object, &block_argument)).to eq ::Set.new([:hello_ruby, :world_ruby]) end end - end.map(&:join) - expect(set).to be_empty + end + end + + context 'concurrency' do + it do + (1..Concurrent::ThreadSafe::Test::THREADS).map do |i| + in_thread do + 1000.times do + v = i + set << v + expect(set).not_to be_empty + set.delete(v) + end + end + end.map(&:join) + expect(set).to be_empty + end end end end