Skip to content

Commit 1f93ad9

Browse files
committed
Simplify TVar implementation at the cost of performance
1 parent a482a35 commit 1f93ad9

File tree

5 files changed

+85
-112
lines changed

5 files changed

+85
-112
lines changed

docs-source/tvar.md

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,9 @@ We implement nested transactions by flattening.
2424
We only support strong isolation if you use the API correctly. In order words,
2525
we do not support strong isolation.
2626

27-
Our implementation uses a very simple two-phased locking with versioned locks
28-
algorithm and lazy writes, as per [1].
29-
30-
See:
31-
32-
1. T. Harris, J. Larus, and R. Rajwar. Transactional Memory. Morgan & Claypool, second edition, 2010.
33-
34-
Note that this implementation allows transactions to continue in a zombie state
35-
with inconsistent reads, so it's possible for the marked exception to be raised
36-
in the example below.
27+
Our implementation uses a very simple algorithm that locks each `TVar` when it
28+
is first read or written. If it cannot lock a `TVar` it aborts and retries.
29+
There is no contention manager so competing transactions may retry eternally.
3730

3831
```ruby
3932
require 'concurrent-ruby'

lib/concurrent-ruby/concurrent/tvar.rb

Lines changed: 20 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ class TVar < Synchronization::Object
1515
# Create a new `TVar` with an initial value.
1616
def initialize(value)
1717
@value = value
18-
@version = 0
1918
@lock = Mutex.new
2019
end
2120

@@ -43,16 +42,6 @@ def unsafe_value=(value) # :nodoc:
4342
@value = value
4443
end
4544

46-
# @!visibility private
47-
def unsafe_version # :nodoc:
48-
@version
49-
end
50-
51-
# @!visibility private
52-
def unsafe_increment_version # :nodoc:
53-
@version += 1
54-
end
55-
5645
# @!visibility private
5746
def unsafe_lock # :nodoc:
5847
@lock
@@ -164,86 +153,57 @@ class Transaction
164153

165154
ABORTED = ::Object.new
166155

167-
ReadLogEntry = Struct.new(:tvar, :version)
156+
OpenEntry = Struct.new(:value, :modified)
168157

169158
AbortError = Class.new(StandardError)
170159
LeaveError = Class.new(StandardError)
171160

172161
def initialize
173-
@read_log = []
174-
@write_log = {}
162+
@open_tvars = {}
175163
end
176164

177165
def read(tvar)
178-
Concurrent::abort_transaction unless valid?
179-
180-
if @write_log.has_key? tvar
181-
@write_log[tvar]
182-
else
183-
@read_log.push(ReadLogEntry.new(tvar, tvar.unsafe_version))
184-
tvar.unsafe_value
185-
end
166+
entry = open(tvar)
167+
entry.value
186168
end
187169

188170
def write(tvar, value)
189-
# Have we already written to this TVar?
171+
entry = open(tvar)
172+
entry.modified = true
173+
entry.value = value
174+
end
190175

191-
if @write_log.has_key? tvar
192-
# Record the value written
193-
@write_log[tvar] = value
194-
else
195-
# Try to lock the TVar
176+
def open(tvar)
177+
entry = @open_tvars[tvar]
196178

179+
unless entry
197180
unless tvar.unsafe_lock.try_lock
198-
# Someone else is writing to this TVar - abort
199181
Concurrent::abort_transaction
200182
end
201183

202-
# Record the value written
203-
204-
@write_log[tvar] = value
205-
206-
# If we previously read from it, check the version hasn't changed
207-
208-
@read_log.each do |log_entry|
209-
if log_entry.tvar == tvar and tvar.unsafe_version > log_entry.version
210-
Concurrent::abort_transaction
211-
end
212-
end
184+
entry = OpenEntry.new(tvar.unsafe_value, false)
185+
@open_tvars[tvar] = entry
213186
end
187+
188+
entry
214189
end
215190

216191
def abort
217192
unlock
218193
end
219194

220195
def commit
221-
return false unless valid?
222-
223-
@write_log.each_pair do |tvar, value|
224-
tvar.unsafe_value = value
225-
tvar.unsafe_increment_version
226-
end
227-
228-
unlock
229-
230-
true
231-
end
232-
233-
def valid?
234-
@read_log.each do |log_entry|
235-
unless @write_log.has_key? log_entry.tvar
236-
if log_entry.tvar.unsafe_version > log_entry.version
237-
return false
238-
end
196+
@open_tvars.each do |tvar, entry|
197+
if entry.modified
198+
tvar.unsafe_value = entry.value
239199
end
240200
end
241201

242-
true
202+
unlock
243203
end
244204

245205
def unlock
246-
@write_log.each_key do |tvar|
206+
@open_tvars.each_key do |tvar|
247207
tvar.unsafe_lock.unlock
248208
end
249209
end

spec/concurrent/tvar_spec.rb

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -106,48 +106,6 @@ module Concurrent
106106
expect(t2.value).to eq 0
107107
end
108108

109-
it 'provides weak isolation' do
110-
t = TVar.new(0)
111-
112-
a = CountDownLatch.new
113-
b = CountDownLatch.new
114-
115-
in_thread do
116-
Concurrent::atomically do
117-
t.value = 1
118-
a.count_down
119-
b.wait
120-
Concurrent.leave_transaction
121-
end
122-
end
123-
124-
Concurrent::atomically do
125-
a.wait
126-
expect(t.value).to eq 0
127-
b.count_down
128-
Concurrent.leave_transaction
129-
end
130-
end
131-
132-
it 'is implemented with lazy writes' do
133-
t = TVar.new(0)
134-
135-
a = CountDownLatch.new
136-
b = CountDownLatch.new
137-
138-
in_thread do
139-
Concurrent::atomically do
140-
t.value = 1
141-
a.count_down
142-
b.wait
143-
end
144-
end
145-
146-
a.wait
147-
expect(t.value).to eq 0
148-
b.count_down
149-
end
150-
151109
it 'nests' do
152110
t = TVar.new(0)
153111

test/stress/tvar/atomicity.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
require 'concurrent-ruby'
2+
3+
v1 = Concurrent::TVar.new(0)
4+
v2 = Concurrent::TVar.new(0)
5+
6+
Thread.new do
7+
loop do
8+
Concurrent.atomically do
9+
v1.value += 1
10+
v2.value += 1
11+
end
12+
end
13+
end
14+
15+
loop do
16+
a, b = Concurrent.atomically {
17+
a = v1.value
18+
b = v2.value
19+
[a, b]
20+
}
21+
raise if a != b
22+
p a
23+
24+
a, b = Concurrent.atomically {
25+
b = v2.value
26+
a = v1.value
27+
[a, b]
28+
}
29+
raise if a != b
30+
p a
31+
end

test/stress/tvar/opacity.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
require 'concurrent-ruby'
2+
3+
v1 = Concurrent::TVar.new(0)
4+
v2 = Concurrent::TVar.new(0)
5+
6+
Thread.new do
7+
loop do
8+
Concurrent.atomically do
9+
v1.value += 1
10+
v2.value += 1
11+
end
12+
end
13+
end
14+
15+
loop do
16+
a, b = Concurrent.atomically {
17+
a = v1.value
18+
b = v2.value
19+
raise if a != b
20+
[a, b]
21+
}
22+
p a
23+
24+
a, b = Concurrent.atomically {
25+
b = v2.value
26+
a = v1.value
27+
raise if a != b
28+
[a, b]
29+
}
30+
p a
31+
end

0 commit comments

Comments
 (0)