Skip to content

Commit 08c2423

Browse files
committed
Add ensure_ivar_visibility! and volatile attributes
1 parent 505d6dd commit 08c2423

File tree

7 files changed

+192
-3
lines changed

7 files changed

+192
-3
lines changed

lib/concurrent/synchronization.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'concurrent/utility/engine'
22
require 'concurrent/synchronization/abstract_object'
33
require 'concurrent/native_extensions' # JavaObject
4+
require 'concurrent/synchronization/java_object' # JavaObject
45
require 'concurrent/synchronization/mutex_object'
56
require 'concurrent/synchronization/monitor_object'
67
require 'concurrent/synchronization/rbx_object'
@@ -21,6 +22,7 @@ module Synchronization
2122
end
2223
private_constant :Implementation
2324

25+
# @see AbstractObject
2426
class Object < Implementation
2527
end
2628
end

lib/concurrent/synchronization/abstract_object.rb

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
module Concurrent
2-
# TODO rename to Synchronization
3-
# TODO add newCondition
42
module Synchronization
53
# Safe synchronization under any Ruby implementation.
6-
# It provides methods like {#synchronize}, {#wait}, {#signal} and {#broadcast}.
4+
# It provides methods like {#synchronize}, {#ns_wait}, {#ns_signal} and {#ns_broadcast}.
75
# Provides a single layer which can improve its implementation over time without changes needed to
86
# the classes using it. Use {Synchronization::Object} not this abstract class.
97
#
@@ -118,6 +116,40 @@ def ns_signal
118116
def ns_broadcast
119117
raise NotImplementedError
120118
end
119+
120+
# Allows to construct immutable objects where all fields are visible after initialization, not requiring
121+
# further synchronization on access.
122+
# @example
123+
# class AClass
124+
# attr_reader :val
125+
# def initialize(val)
126+
# @val = val # final value, after assignment it's not changed (just convention, not enforced)
127+
# ensure_ivar_visibility!
128+
# # now it can be shared as Java's final field
129+
# end
130+
# end
131+
def ensure_ivar_visibility!
132+
raise NotImplementedError
133+
end
134+
135+
# creates methods for reading and writing to a instance variable with volatile (Java semantic) instance variable
136+
# return [Array<Symbol>] names of defined method names
137+
def self.attr_volatile(*names)
138+
names.each do |name|
139+
ivar = :"@volatile_#{name}"
140+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
141+
def #{name}
142+
#{ivar}
143+
end
144+
145+
def #{name}=(value)
146+
#{ivar} = value
147+
end
148+
RUBY
149+
end
150+
names.map { |n| [n, :"#{n}="] }.flatten
151+
end
152+
121153
end
122154
end
123155
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Concurrent
2+
module Synchronization
3+
4+
if Concurrent.on_jruby?
5+
require 'jruby'
6+
7+
class JavaObject < AbstractObject
8+
def ensure_ivar_visibility!
9+
# relying on undocumented behavior of JRuby, ivar access is volatile
10+
end
11+
end
12+
end
13+
end
14+
end

lib/concurrent/synchronization/java_pure_object.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ def ns_signal
3333
JRuby.reference0(self).notify
3434
self
3535
end
36+
37+
def ensure_ivar_visibility!
38+
# relying on undocumented behavior of JRuby, ivar access is volatile
39+
end
3640
end
3741
end
3842
end

lib/concurrent/synchronization/mutex_object.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def ns_wait(timeout = nil)
3131
@__condition__do_not_use_directly.wait @__lock__do_not_use_directly, timeout
3232
self
3333
end
34+
35+
def ensure_ivar_visibility!
36+
# relying on undocumented behavior of CRuby, GVL acquire has lock which ensures visibility of ivars
37+
# https://github.com/ruby/ruby/blob/ruby_2_2/thread_pthread.c#L204-L211
38+
end
3439
end
3540
end
3641
end

lib/concurrent/synchronization/rbx_object.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,29 @@ def ns_broadcast
4444
@waiters.shift << true until @waiters.empty?
4545
self
4646
end
47+
48+
def ensure_ivar_visibility!
49+
# Rubinius instance variables are not volatile so we need to insert barrier
50+
Rubinius.memory_barrier
51+
end
52+
53+
def self.attr_volatile *names
54+
names.each do |name|
55+
ivar = :"@volatile_#{name}"
56+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
57+
def #{name}
58+
Rubinius.memory_barrier
59+
#{ivar}
60+
end
61+
62+
def #{name}=(value)
63+
#{ivar} = value
64+
Rubinius.memory_barrier
65+
end
66+
RUBY
67+
end
68+
names.map { |n| [n, :"#{n}="] }.flatten
69+
end
4770
end
4871
end
4972
end
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
module Concurrent
2+
3+
describe Synchronization::Object do
4+
5+
class AClass < Synchronization::Object
6+
attr_volatile :volatile
7+
attr_accessor :not_volatile
8+
9+
def initialize(value = nil)
10+
super()
11+
@Final = value
12+
ensure_ivar_visibility!
13+
end
14+
15+
def final
16+
@Final
17+
end
18+
19+
def count
20+
synchronize { @count += 1 }
21+
end
22+
23+
def wait(timeout = nil)
24+
synchronize { ns_wait(timeout) }
25+
end
26+
27+
private
28+
29+
def ns_initialize
30+
@count = 0
31+
end
32+
end
33+
34+
subject { AClass.new }
35+
36+
describe '#wait' do
37+
38+
it 'waiting thread is sleeping' do
39+
t = Thread.new do
40+
Thread.abort_on_exception = true
41+
subject.wait
42+
end
43+
sleep 0.1
44+
expect(t.status).to eq 'sleep'
45+
end
46+
47+
it 'sleeping thread can be killed' do
48+
t = Thread.new do
49+
Thread.abort_on_exception = true
50+
subject.wait rescue nil
51+
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
57+
end
58+
end
59+
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
65+
end
66+
end
67+
68+
describe 'signaling' do
69+
pending 'for now pending, tested pretty well by Event'
70+
end
71+
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
79+
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
91+
end
92+
end
93+
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
100+
end
101+
end
102+
103+
t2.join
104+
t1.kill
105+
end
106+
end
107+
108+
end
109+
end

0 commit comments

Comments
 (0)