Skip to content

Commit 8a4f225

Browse files
committed
TVar: motivation in documentation.
1 parent b6667be commit 8a4f225

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed

md/tvar.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,121 @@ we do not support strong isolation.
2929
See:
3030

3131
1. T. Harris, J. Larus, and R. Rajwar. Transactional Memory. Morgan & Claypool, second edition, 2010.
32+
33+
## Motivation
34+
35+
Consider an application that transfers money between bank accounts. We want to
36+
transfer money from one account to another. It is very important that we don't
37+
lose any money! But it is also important that we can handle many account
38+
transfers at the same time, so we run them concurrently, and probably also in
39+
parallel.
40+
41+
This code shows us transferring ten pounds from one account to another.
42+
43+
```ruby
44+
a = new BankAccount(100_000)
45+
b = new BankAccount(100)
46+
47+
a.value -= 10
48+
b.value += 10
49+
```
50+
51+
Before we even start to talk about to talk about concurrency and parallelism, is
52+
this code safe? What happens if after removing money from account a, we get an
53+
exception? It's a slightly contrived example, but if the account totals were
54+
very large, adding to them could involve the stack allocation of a `BigNum`, and
55+
so could cause out of memory exceptions. In that case the money would have
56+
disappeared from account a, but not appeared in account b. Disaster!
57+
58+
So what do we really need to do?
59+
60+
```ruby
61+
a = new BankAccount(100_000)
62+
b = new BankAccount(100)
63+
64+
original_a = a.value
65+
a.value -= 10
66+
67+
begin
68+
b.value += 10
69+
rescue e =>
70+
a.value = original_a
71+
raise e
72+
end
73+
```
74+
75+
This rescues any exceptions raised when setting b and will roll back the change
76+
we have already made to b. We'll keep this rescue code in mind, but we'll leave
77+
it out of future examples for simplicity.
78+
79+
That might have made the code work when it only runs sequentially. Lets start to
80+
consider some concurrency. It's obvious that we want to make the transfer of
81+
money mutually exclusive with any other transfers - in order words it is a
82+
critical section.
83+
84+
The usual solution to this would be to use a lock.
85+
86+
```ruby
87+
lock.synchronize do
88+
a.value -= 10
89+
b.value += 10
90+
end
91+
```
92+
93+
That should work. Except we said we'd like these transfer to run concurrently,
94+
and in parallel. With a single lock like that we'll only let one transfer take
95+
place at a time. Perhaps we need more locks? We could have one per account
96+
97+
```ruby
98+
a.lock.synchronize do
99+
b.lock.synchronize do
100+
a.value -= 10
101+
b.value += 10
102+
end
103+
end
104+
```
105+
106+
However this is vulnerable to deadlock. If we tried to transfer from a to b, at
107+
the same time as from b to a, it's possible that the first transfer locks a, the
108+
second transfer locks b, and then they both sit there waiting forever to get the
109+
other lock. Perhaps we can solve that by applying a total ordering to the locks
110+
- always acquire them in the same order?
111+
112+
```ruby
113+
locks_needed = [a.lock, b.lock]
114+
locks_in_order = locks_needed.sort{ |x, y| x.number <=> y.number }
115+
116+
locks_in_order[0].synchronize do
117+
locks_in_order[1].synchronize do
118+
a.value -= 10
119+
b.value += 10
120+
end
121+
end
122+
```
123+
124+
That might work. But we need to know exactly what locks we're going to need
125+
before we start. If there were conditions in side the transfer this might be
126+
more complicated. We also need to remember the rescue code we had above to deal
127+
with exceptions. This is getting out of hand - and it's where `TVar` comes in.
128+
129+
We'll model the accounts as `TVar` - transactional variable, and instead of
130+
locks we'll use `Concurrent::atomically`.
131+
132+
```ruby
133+
a = new TVar(100_000)
134+
b = new TVar(100)
135+
136+
Concurrent::atomically do
137+
a.value -= 10
138+
b.value += 10
139+
end
140+
```
141+
142+
That short piece of code effectively solves all the concerns we identified
143+
above. How it does it is described in the reference above. You just need to be
144+
happy that any two `atomically` blocks (we call them transactions) that use an
145+
overlapping set of `TVar` objects will appear to have happened as if there was a
146+
big global lock on them, and that if any exception is raised in the block, it
147+
will be as if the block never happened. But also keep in mind the important
148+
points we detailed right at the start of the article about side effects and
149+
repeated execution.

0 commit comments

Comments
 (0)