Skip to content

Commit 5505768

Browse files
committed
Implemented ImmutableStruct, MutableStruct, and Settable Struct.
1 parent 759a1ef commit 5505768

17 files changed

+1509
-93
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ This library contains a variety of concurrency abstractions at high and low leve
6363

6464
* See [ThreadPool](http://ruby-concurrency.github.io/concurrent-ruby/file.thread_pools.html) overview, which also contains a list of other Executors available.
6565

66+
* Thread-safe structure classes derived from Ruby's [Struct](http://ruby-doc.org/core-2.2.0/Struct.html)
67+
68+
* `ImmutableStruct` Immutable struct where values are set at construction and cannot be changed later.
69+
* `MutableStruct` Synchronized, mutable struct where values can be safely changed at any time.
70+
* `SettableStruct` Synchronized, write-once struct where values can be set at most once, either at construction or any time thereafter.
71+
6672
### Thread synchronization classes and algorithms
6773

6874
* [CountdownLatch](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/CountDownLatch.html)

examples/benchmark_structs.rb

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env ruby
2+
3+
$: << File.expand_path('../../lib', __FILE__)
4+
5+
require 'benchmark'
6+
require 'concurrent'
7+
8+
n = 500_000
9+
10+
StructPair = Struct.new(:left, :right)
11+
SafePair = Concurrent::MutableStruct.new(:left, :right)
12+
FinalPair = Concurrent::SettableStruct.new(:left, :right)
13+
ImmutablePair = Concurrent::ImmutableStruct.new(:left, :right)
14+
15+
array_pair = [true, false].freeze
16+
struct_pair = StructPair.new(true, false)
17+
safe_pair = SafePair.new(true, false)
18+
final_pair = FinalPair.new(true, false)
19+
immutable = ImmutablePair.new(true, false)
20+
21+
puts "Object creation...\n"
22+
Benchmark.bmbm do |x|
23+
x.report('create frozen array') { n.times{ [true, false].freeze } }
24+
x.report('create frozen struct') { n.times{ StructPair.new(true, false).freeze } }
25+
x.report('create mutable struct') { n.times{ SafePair.new(true, false) } }
26+
x.report('create settable struct') { n.times{ FinalPair.new(true, false) } }
27+
x.report('create immutable struct') { n.times{ ImmutablePair.new(true, false) } }
28+
end
29+
30+
puts "\n"
31+
32+
puts "Object access...\n"
33+
Benchmark.bmbm do |x|
34+
x.report('read from frozen array') { n.times{ array_pair.last } }
35+
x.report('read from frozen struct') { n.times{ struct_pair.right } }
36+
x.report('read from mutable struct') { n.times{ safe_pair.right } }
37+
x.report('read from settable struct') { n.times{ final_pair.right } }
38+
x.report('read from immutable struct') { n.times{ immutable.right } }
39+
end
40+
41+
puts "\n"
42+
43+
puts "Enumeration...\n"
44+
Benchmark.bmbm do |x|
45+
x.report('iterate over frozen array') { n.times{ array_pair.each{ nil } } }
46+
x.report('iterate over frozen struct') { n.times{ struct_pair.each{ nil } } }
47+
x.report('iterate over mutable struct') { n.times{ safe_pair.each{ nil } } }
48+
x.report('iterate over settable struct') { n.times{ final_pair.each{ nil } } }
49+
x.report('iterate over immutable struct') { n.times{ immutable.each{ nil } } }
50+
end
51+
52+
__END__
53+
54+
Object creation...
55+
Rehearsal -----------------------------------------------------------
56+
create frozen array 0.090000 0.000000 0.090000 ( 0.091262)
57+
create frozen struct 0.180000 0.000000 0.180000 ( 0.179993)
58+
create mutable struct 2.030000 0.000000 2.030000 ( 2.052071)
59+
create settable struct 2.070000 0.000000 2.070000 ( 2.080022)
60+
create immutable struct 0.710000 0.000000 0.710000 ( 0.716877)
61+
-------------------------------------------------- total: 5.080000sec
62+
63+
user system total real
64+
create frozen array 0.100000 0.000000 0.100000 ( 0.097776)
65+
create frozen struct 0.190000 0.000000 0.190000 ( 0.186287)
66+
create mutable struct 2.020000 0.010000 2.030000 ( 2.032391)
67+
create settable struct 2.030000 0.000000 2.030000 ( 2.031631)
68+
create immutable struct 0.690000 0.000000 0.690000 ( 0.695010)
69+
70+
Object access...
71+
Rehearsal --------------------------------------------------------------
72+
read from frozen array 0.060000 0.000000 0.060000 ( 0.060430)
73+
read from frozen struct 0.060000 0.000000 0.060000 ( 0.058978)
74+
read from mutable struct 0.440000 0.000000 0.440000 ( 0.454071)
75+
read from settable struct 0.460000 0.000000 0.460000 ( 0.457699)
76+
read from immutable struct 0.120000 0.000000 0.120000 ( 0.126701)
77+
----------------------------------------------------- total: 1.140000sec
78+
79+
user system total real
80+
read from frozen array 0.060000 0.000000 0.060000 ( 0.063006)
81+
read from frozen struct 0.060000 0.000000 0.060000 ( 0.094203)
82+
read from mutable struct 0.420000 0.000000 0.420000 ( 0.468304)
83+
read from settable struct 0.410000 0.000000 0.410000 ( 0.452446)
84+
read from immutable struct 0.110000 0.010000 0.120000 ( 0.127030)
85+
86+
Enumeration...
87+
Rehearsal -----------------------------------------------------------------
88+
iterate over frozen array 0.170000 0.000000 0.170000 ( 0.176898)
89+
iterate over frozen struct 0.160000 0.000000 0.160000 ( 0.160786)
90+
iterate over mutable struct 1.520000 0.000000 1.520000 ( 1.627013)
91+
iterate over settable struct 1.500000 0.010000 1.510000 ( 1.525163)
92+
iterate over immutable struct 0.990000 0.000000 0.990000 ( 1.006201)
93+
-------------------------------------------------------- total: 4.350000sec
94+
95+
user system total real
96+
iterate over frozen array 0.170000 0.000000 0.170000 ( 0.167927)
97+
iterate over frozen struct 0.150000 0.000000 0.150000 ( 0.157328)
98+
iterate over mutable struct 1.450000 0.000000 1.450000 ( 1.462654)
99+
iterate over settable struct 1.460000 0.000000 1.460000 ( 1.480270)
100+
iterate over immutable struct 0.940000 0.010000 0.950000 ( 0.955633)

lib/concurrent.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require 'concurrent/errors'
1111
require 'concurrent/executors'
1212
require 'concurrent/utilities'
13+
require 'concurrent/struct'
1314

1415
require 'concurrent/atomic/atomic_reference'
1516
require 'concurrent/async'

lib/concurrent/atomic/atomic_fixnum.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class MutexAtomicFixnum < Synchronization::Object
3232
#
3333
# Creates a new `AtomicFixnum` with the given initial value.
3434
#
35-
# @param [Fixnum] init the initial value
35+
# @param [Fixnum] initial the initial value
3636
# @raise [ArgumentError] if the initial value is not a `Fixnum`
3737
def initialize(initial = 0)
3838
super(initial)

lib/concurrent/errors.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ module Concurrent
77
# sequence or when the object is in an inappropriate state.
88
LifecycleError = Class.new(StandardError)
99

10+
# Raised when an attempt is made to violate an immutability guarantee.
11+
ImmutabilityError = Class.new(StandardError)
12+
1013
# Raised when an object's methods are called when it has not been
1114
# properly initialized.
1215
InitializationError = Class.new(StandardError)

lib/concurrent/struct.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
require 'concurrent/struct/immutable_struct'
2+
require 'concurrent/struct/mutable_struct'
3+
require 'concurrent/struct/settable_struct'
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
module Concurrent
2+
3+
# @!visibility private
4+
module AbstractStruct
5+
6+
# @!macro [attach] struct_length
7+
#
8+
# Returns the number of struct members.
9+
#
10+
# @return [Fixnum] the number of struct members
11+
def length
12+
self.class::MEMBERS.length
13+
end
14+
alias_method :size, :length
15+
16+
# @!macro [attach] struct_members
17+
#
18+
# Returns the struct members as an array of symbols.
19+
#
20+
# @return [Array] the struct members as an array of symbols
21+
def members
22+
self.class::MEMBERS.dup
23+
end
24+
25+
protected
26+
27+
# @!macro struct_values
28+
#
29+
# @!visibility private
30+
def ns_values
31+
@values.dup
32+
end
33+
34+
# @!macro struct_values_at
35+
#
36+
# @!visibility private
37+
def ns_values_at(indexes)
38+
@values.values_at(*indexes)
39+
end
40+
41+
# @!macro struct_to_h
42+
#
43+
# @!visibility private
44+
def ns_to_h
45+
length.times.reduce({}){|memo, i| memo[self.class::MEMBERS[i]] = @values[i]; memo}
46+
end
47+
48+
# @!macro struct_get
49+
#
50+
# @!visibility private
51+
def ns_get(member)
52+
if member.is_a? Integer
53+
if member >= @values.length
54+
raise IndexError.new("offset #{member} too large for struct(size:#{@values.length})")
55+
end
56+
@values[member]
57+
else
58+
send(member)
59+
end
60+
rescue NoMethodError
61+
raise NameError.new("no member '#{member}' in struct")
62+
end
63+
64+
# @!macro struct_equality
65+
#
66+
# @!visibility private
67+
def ns_equality(other)
68+
self.class == other.class && self.values == other.values
69+
end
70+
71+
# @!macro struct_each
72+
#
73+
# @!visibility private
74+
def ns_each
75+
values.each{|value| yield value }
76+
end
77+
78+
# @!macro struct_each_pair
79+
#
80+
# @!visibility private
81+
def ns_each_pair
82+
@values.length.times do |index|
83+
yield self.class::MEMBERS[index], @values[index]
84+
end
85+
end
86+
87+
# @!macro struct_select
88+
#
89+
# @!visibility private
90+
def ns_select
91+
values.select{|value| yield value }
92+
end
93+
94+
# @!macro struct_inspect
95+
#
96+
# @!visibility private
97+
def ns_inspect
98+
struct = pr_underscore(self.class.ancestors[1])
99+
clazz = ((self.class.to_s =~ /^#<Class:/) == 0) ? '' : " #{self.class}"
100+
"#<#{struct}#{clazz} #{ns_to_h}>"
101+
end
102+
103+
# @!macro struct_merge
104+
#
105+
# @!visibility private
106+
def ns_merge(other, &block)
107+
self.class.new(*self.to_h.merge(other, &block).values)
108+
end
109+
110+
# @!visibility private
111+
def pr_underscore(clazz)
112+
word = clazz.to_s
113+
word.gsub!(/::/, '/')
114+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
115+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
116+
word.tr!("-", "_")
117+
word.downcase!
118+
word
119+
end
120+
121+
# @!visibility private
122+
def self.define_struct_class(parent, base, name, members, &block)
123+
clazz = Class.new(base || Object) do
124+
include parent
125+
self.const_set(:MEMBERS, members.collect{|member| member.to_s.to_sym}.freeze)
126+
def ns_initialize(*values)
127+
raise ArgumentError.new('struct size differs') if values.length > length
128+
@values = values.fill(nil, values.length..length-1)
129+
end
130+
end
131+
unless name.nil?
132+
begin
133+
parent.const_set(name, clazz)
134+
parent.const_get(name)
135+
rescue NameError
136+
raise NameError.new("identifier #{name} needs to be constant")
137+
end
138+
end
139+
members.each_with_index do |member, index|
140+
clazz.send(:define_method, member) do
141+
@values[index]
142+
end
143+
end
144+
clazz.class_exec(&block) unless block.nil?
145+
clazz
146+
end
147+
end
148+
end
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
require 'concurrent/struct/abstract_struct'
2+
require 'concurrent/synchronization'
3+
4+
module Concurrent
5+
6+
# A thread-safe, immutable variation of Ruby's standard `Struct`.
7+
#
8+
# @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct`
9+
module ImmutableStruct
10+
include AbstractStruct
11+
12+
# @!visibility private
13+
def initialize(*values)
14+
ns_initialize(*values)
15+
end
16+
17+
# @!macro struct_values
18+
def values
19+
ns_values
20+
end
21+
alias_method :to_a, :values
22+
23+
# @!macro struct_values_at
24+
def values_at(*indexes)
25+
ns_values_at(indexes)
26+
end
27+
28+
# @!macro struct_inspect
29+
def inspect
30+
ns_inspect
31+
end
32+
alias_method :to_s, :inspect
33+
34+
# @!macro struct_merge
35+
def merge(other, &block)
36+
ns_merge(other, &block)
37+
end
38+
39+
# @!macro struct_to_h
40+
def to_h
41+
ns_to_h
42+
end
43+
44+
# @!macro struct_get
45+
def [](member)
46+
ns_get(member)
47+
end
48+
49+
# @!macro struct_equality
50+
def ==(other)
51+
ns_equality(other)
52+
end
53+
54+
# @!macro struct_each
55+
def each(&block)
56+
return enum_for(:each) unless block_given?
57+
ns_each(&block)
58+
end
59+
60+
# @!macro struct_each_pair
61+
def each_pair(&block)
62+
return enum_for(:each_pair) unless block_given?
63+
ns_each_pair(&block)
64+
end
65+
66+
# @!macro struct_select
67+
def select(&block)
68+
return enum_for(:select) unless block_given?
69+
ns_select(&block)
70+
end
71+
72+
# @!macro struct_new
73+
def self.new(*args, &block)
74+
clazz_name = nil
75+
if args.length == 0
76+
raise ArgumentError.new('wrong number of arguments (0 for 1+)')
77+
elsif args.length > 0 && args.first.is_a?(String)
78+
clazz_name = args.shift
79+
end
80+
FACTORY.define_struct(clazz_name, args, &block)
81+
end
82+
83+
FACTORY = Class.new(Synchronization::Object) do
84+
def define_struct(name, members, &block)
85+
synchronize do
86+
AbstractStruct.define_struct_class(ImmutableStruct, nil, name, members, &block)
87+
end
88+
end
89+
end.new
90+
private_constant :FACTORY
91+
end
92+
end

0 commit comments

Comments
 (0)