Skip to content

Commit 1ba4f88

Browse files
committed
ImmutableStruct Improvements
1 parent 5c4425f commit 1ba4f88

File tree

2 files changed

+141
-82
lines changed

2 files changed

+141
-82
lines changed
Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,67 @@
11
module Concurrent
22
module Synchronization
3+
# Similar to Struct but the fields are immutable and always visible (like Java final fields)
4+
# @example
5+
# Person = ImmutableStruct.with_fields :name, :age
6+
# Person.new 'John Doe', 15
7+
# Person['John Doe', 15]
8+
# Person['John Doe', 15].members # => [:name, :age]
9+
# Person['John Doe', 15].values # => ['John Doe', 15]
310
class ImmutableStruct < Synchronization::Object
411
def self.with_fields(*names, &block)
512
Class.new(self) do
613
attr_reader(*names)
7-
instance_eval &block if block
814

915
class_eval <<-RUBY, __FILE__, __LINE__ + 1
1016
def initialize(#{names.join(', ')})
1117
#{names.map { |n| '@' + n.to_s }.join(', ')} = #{names.join(', ')}
1218
ensure_ivar_visibility!
1319
end
20+
21+
def members
22+
#{names.inspect}
23+
end
24+
25+
def self.members
26+
#{names.inspect}
27+
end
1428
RUBY
29+
30+
instance_eval &block if block
1531
end
1632
end
1733

34+
# Define equality based on class and members' equality. This is optional since for CAS operation
35+
# it may be required to compare references which is default behaviour of this class.
36+
def self.define_equality!
37+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
38+
def ==(other)
39+
self.class == other.class &&
40+
#{members.map { |name| "self.#{name} == other.#{name}" }.join(" && ")}
41+
end
42+
RUBY
43+
end
44+
1845
def self.[](*args)
1946
new *args
2047
end
48+
49+
include Enumerable
50+
51+
def each(&block)
52+
return to_enum unless block_given?
53+
members.zip(values).each(&block)
54+
end
55+
56+
def size
57+
members.size
58+
end
59+
60+
def values
61+
members.map { |name| send name }
62+
end
63+
64+
alias_method :to_a, :values
2165
end
2266
end
2367
end

spec/concurrent/synchronized_object_spec.rb

Lines changed: 96 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,133 @@
11
module Concurrent
22

3-
describe Synchronization::Object do
3+
describe Synchronization do
4+
describe Synchronization::Object do
45

5-
class AClass < Synchronization::Object
6-
attr_volatile :volatile
7-
attr_accessor :not_volatile
6+
class AClass < Synchronization::Object
7+
attr_volatile :volatile
8+
attr_accessor :not_volatile
89

9-
def initialize(value = nil)
10-
super()
11-
@Final = value
12-
ensure_ivar_visibility!
13-
end
10+
def initialize(value = nil)
11+
super()
12+
@Final = value
13+
ensure_ivar_visibility!
14+
end
1415

15-
def final
16-
@Final
17-
end
16+
def final
17+
@Final
18+
end
1819

19-
def count
20-
synchronize { @count += 1 }
21-
end
20+
def count
21+
synchronize { @count += 1 }
22+
end
2223

23-
def wait(timeout = nil)
24-
synchronize { ns_wait(timeout) }
25-
end
24+
def wait(timeout = nil)
25+
synchronize { ns_wait(timeout) }
26+
end
2627

27-
private
28+
private
2829

29-
def ns_initialize
30-
@count = 0
30+
def ns_initialize
31+
@count = 0
32+
end
3133
end
32-
end
3334

34-
subject { AClass.new }
35+
subject { AClass.new }
3536

36-
describe '#wait' do
37+
describe '#wait' do
3738

38-
it 'waiting thread is sleeping' do
39-
t = Thread.new do
40-
Thread.abort_on_exception = true
41-
subject.wait
39+
it 'waiting thread is sleeping' do
40+
t = Thread.new do
41+
Thread.abort_on_exception = true
42+
subject.wait
43+
end
44+
sleep 0.1
45+
expect(t.status).to eq 'sleep'
4246
end
43-
sleep 0.1
44-
expect(t.status).to eq 'sleep'
45-
end
4647

47-
it 'sleeping thread can be killed' do
48-
t = Thread.new do
49-
Thread.abort_on_exception = true
50-
subject.wait rescue nil
48+
it 'sleeping thread can be killed' do
49+
t = Thread.new do
50+
Thread.abort_on_exception = true
51+
subject.wait rescue nil
52+
end
53+
sleep 0.1
54+
t.kill
55+
sleep 0.1
56+
expect(t.status).to eq false
57+
expect(t.alive?).to eq false
5158
end
52-
sleep 0.1
53-
t.kill
54-
sleep 0.1
55-
expect(t.status).to eq false
56-
expect(t.alive?).to eq false
5759
end
58-
end
5960

60-
describe '#synchronize' do
61-
it 'allows only one thread to execute count' do
62-
threads = 10.times.map { Thread.new(subject) { 100.times { subject.count } } }
63-
threads.each(&:join)
64-
expect(subject.count).to eq 1001
61+
describe '#synchronize' do
62+
it 'allows only one thread to execute count' do
63+
threads = 10.times.map { Thread.new(subject) { 100.times { subject.count } } }
64+
threads.each(&:join)
65+
expect(subject.count).to eq 1001
66+
end
6567
end
66-
end
6768

68-
describe 'signaling' do
69-
pending 'for now pending, tested pretty well by Event'
70-
end
69+
describe 'signaling' do
70+
pending 'for now pending, tested pretty well by Event'
71+
end
7172

72-
specify 'final field always visible' do
73-
store = AClass.new 'asd'
74-
t1 = Thread.new { 1000000000.times { |i| store = AClass.new i.to_s } }
75-
t2 = Thread.new { 10.times { expect(store.final).not_to be_nil; Thread.pass } }
76-
t2.join
77-
t1.kill
78-
end
73+
specify 'final field always visible' do
74+
store = AClass.new 'asd'
75+
t1 = Thread.new { 1000000000.times { |i| store = AClass.new i.to_s } }
76+
t2 = Thread.new { 10.times { expect(store.final).not_to be_nil; Thread.pass } }
77+
t2.join
78+
t1.kill
79+
end
7980

80-
describe 'attr volatile' do
81-
specify 'older writes are always visible' do
82-
store = AClass.new
83-
store.not_volatile = 0
84-
store.volatile = 0
85-
86-
t1 = Thread.new do
87-
Thread.abort_on_exception = true
88-
1000000000.times do |i|
89-
store.not_volatile = i
90-
store.volatile = i
81+
describe 'attr volatile' do
82+
specify 'older writes are always visible' do
83+
store = AClass.new
84+
store.not_volatile = 0
85+
store.volatile = 0
86+
87+
t1 = Thread.new do
88+
Thread.abort_on_exception = true
89+
1000000000.times do |i|
90+
store.not_volatile = i
91+
store.volatile = i
92+
end
9193
end
92-
end
9394

94-
t2 = Thread.new do
95-
10.times do
96-
volatile = store.volatile
97-
not_volatile = store.not_volatile
98-
expect(not_volatile).to be >= volatile
99-
Thread.pass
95+
t2 = Thread.new do
96+
10.times do
97+
volatile = store.volatile
98+
not_volatile = store.not_volatile
99+
expect(not_volatile).to be >= volatile
100+
Thread.pass
101+
end
100102
end
101-
end
102103

103-
t2.join
104-
t1.kill
104+
t2.join
105+
t1.kill
106+
end
105107
end
106108
end
107109

108110
describe Synchronization::ImmutableStruct do
109-
let(:klass) { described_class.with_fields(:a, :b) }
110-
subject { klass[1, 'a'] }
111+
AB = described_class.with_fields(:a, :b)
112+
subject { AB[1, 'a'] }
111113

112114
specify do
113-
expect(klass.superclass).to eq described_class
115+
expect(AB.superclass).to eq described_class
114116
expect(subject.a).to eq 1
115117
expect(subject.b).to eq 'a'
118+
expect(subject.values).to eq [1, 'a']
119+
expect(subject.to_a).to eq [1, 'a']
120+
expect(subject.size).to eq 2
121+
expect(subject.members).to eq [:a, :b]
122+
expect(subject.each.to_a).to eq [[:a, 1], [:b, 'a']]
123+
expect(subject.inspect).to match /#<Concurrent::AB:0x[\da-f]+ (@a=1|@b="a"), (@a=1|@b="a")>/
124+
end
125+
126+
specify 'equality' do
127+
klass = described_class.with_fields(:a, :b)
128+
expect(klass[1, 'a']).not_to be == klass[1, 'a']
129+
klass.define_equality!
130+
expect(klass[1, 'a']).to be == klass[1, 'a']
116131
end
117132
end
118133

0 commit comments

Comments
 (0)