Skip to content

Commit ecc984e

Browse files
committed
TVar: documentation.
1 parent 8ba37c9 commit ecc984e

File tree

2 files changed

+114
-74
lines changed

2 files changed

+114
-74
lines changed

lib/concurrent/tvar.rb

Lines changed: 113 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -4,57 +4,158 @@
44

55
module Concurrent
66

7-
ABORTED = Object.new
8-
9-
CURRENT_TRANSACTION = ThreadLocalVar.new(nil)
10-
11-
ReadLogEntry = Struct.new(:tvar, :version)
12-
UndoLogEntry = Struct.new(:tvar, :value)
13-
7+
# A `TVar` is a transactional variable - a single-element container that
8+
# is used as part of a transaction - see `Concurrent::atomically`.
149
class TVar
1510

11+
# Create a new `TVar` with an initial value.
1612
def initialize(value)
1713
@value = value
1814
@version = 0
1915
@lock = Mutex.new
2016
end
2117

18+
# Get the value of a `TVar`.
2219
def value
2320
Concurrent::atomically do
2421
Transaction::current.read(self)
2522
end
2623
end
2724

25+
# Set the value of a `TVar`.
2826
def value=(value)
2927
Concurrent::atomically do
3028
Transaction::current.write(self, value)
3129
end
3230
end
3331

34-
def unsafe_value
32+
# @!visibility private
33+
def unsafe_value # :nodoc:
3534
@value
3635
end
3736

38-
def unsafe_value=(value)
37+
# @!visibility private
38+
def unsafe_value=(value) # :nodoc:
3939
@value = value
4040
end
4141

42-
def unsafe_version
42+
# @!visibility private
43+
def unsafe_version # :nodoc:
4344
@version
4445
end
4546

46-
def unsafe_increment_version
47+
# @!visibility private
48+
def unsafe_increment_version # :nodoc:
4749
@version += 1
4850
end
4951

50-
def unsafe_lock
52+
# @!visibility private
53+
def unsafe_lock # :nodoc:
5154
@lock
5255
end
5356

5457
end
5558

59+
# Run a block that reads and writes `TVar`s as a single atomic transaction.
60+
# With respect to the value of `TVar` objects, the transaction is atomic,
61+
# in that it either happens or it does not, consistent, in that the `TVar`
62+
# objects involved will never enter an illegal state, and isolated, in that
63+
# transactions never interfere with each other. You may recognise these
64+
# properties from database transactions.
65+
#
66+
# There are some very important and unusual semantics that you must be aware of:
67+
#
68+
# * Most importantly, the block that you pass to atomically may be executed more than once. In most cases your code should be free of side-effects, except for via TVar.
69+
#
70+
# * If an exception escapes an atomically block it will abort the transaction.
71+
#
72+
# * It is undefined behaviour to use callcc or Fiber with atomically.
73+
#
74+
# * If you create a new thread within an atomically, it will not be part of the transaction. Creating a thread counts as a side-effect.
75+
#
76+
# Transactions within transactions are flattened to a single transaction.
77+
#
78+
# @example
79+
# a = new TVar(100_000)
80+
# b = new TVar(100)
81+
#
82+
# Concurrent::atomically do
83+
# a.value -= 10
84+
# b.value += 10
85+
# end
86+
def atomically
87+
raise ArgumentError.new('no block given') unless block_given?
88+
89+
# Get the current transaction
90+
91+
transaction = Transaction::current
92+
93+
# Are we not already in a transaction (not nested)?
94+
95+
if transaction.nil?
96+
# New transaction
97+
98+
begin
99+
# Retry loop
100+
101+
loop do
102+
103+
# Create a new transaction
104+
105+
transaction = Transaction.new
106+
Transaction::current = transaction
107+
108+
# Run the block, aborting on exceptions
109+
110+
begin
111+
result = yield
112+
rescue Transaction::AbortError => e
113+
transaction.abort
114+
result = Transaction::ABORTED
115+
rescue => e
116+
transaction.abort
117+
throw e
118+
end
119+
# If we can commit, break out of the loop
120+
121+
if result != Transaction::ABORTED
122+
if transaction.commit
123+
break result
124+
end
125+
end
126+
end
127+
ensure
128+
# Clear the current transaction
129+
130+
Transaction::current = nil
131+
end
132+
else
133+
# Nested transaction - flatten it and just run the block
134+
135+
yield
136+
end
137+
end
138+
139+
# Abort a currently running transaction - see `Concurrent::atomically`.
140+
def abort_transaction
141+
raise Transaction::AbortError.new
142+
end
143+
144+
module_function :atomically, :abort_transaction
145+
146+
private
147+
56148
class Transaction
57149

150+
ABORTED = Object.new
151+
152+
CURRENT_TRANSACTION = ThreadLocalVar.new(nil)
153+
154+
ReadLogEntry = Struct.new(:tvar, :version)
155+
UndoLogEntry = Struct.new(:tvar, :value)
156+
157+
AbortError = Class.new(StandardError)
158+
58159
def initialize
59160
@write_set = Set.new
60161
@read_log = []
@@ -148,65 +249,4 @@ def self.current=(transaction)
148249

149250
end
150251

151-
AbortError = Class.new(StandardError)
152-
153-
def atomically
154-
raise ArgumentError.new('no block given') unless block_given?
155-
156-
# Get the current transaction
157-
158-
transaction = Transaction::current
159-
160-
# Are we not already in a transaction (not nested)?
161-
162-
if transaction.nil?
163-
# New transaction
164-
165-
begin
166-
# Retry loop
167-
168-
loop do
169-
170-
# Create a new transaction
171-
172-
transaction = Transaction.new
173-
Transaction::current = transaction
174-
175-
# Run the block, aborting on exceptions
176-
177-
begin
178-
result = yield
179-
rescue AbortError => e
180-
transaction.abort
181-
result = ABORTED
182-
rescue => e
183-
transaction.abort
184-
throw e
185-
end
186-
# If we can commit, break out of the loop
187-
188-
if result != ABORTED
189-
if transaction.commit
190-
break result
191-
end
192-
end
193-
end
194-
ensure
195-
# Clear the current transaction
196-
197-
Transaction::current = nil
198-
end
199-
else
200-
# Nested transaction - flatten it and just run the block
201-
202-
yield
203-
end
204-
end
205-
206-
def abort_transaction
207-
raise AbortError.new
208-
end
209-
210-
module_function :atomically, :abort_transaction
211-
212252
end

spec/concurrent/tvar_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ module Concurrent
129129
describe '#abort_transaction' do
130130

131131
it 'raises an exception outside an #atomically block' do
132-
expect { Concurrent::abort_transaction }.to raise_error(Concurrent::AbortError)
132+
expect { Concurrent::abort_transaction }.to raise_error(Concurrent::Transaction::AbortError)
133133
end
134134

135135
end

0 commit comments

Comments
 (0)