@@ -29,3 +29,121 @@ we do not support strong isolation.
29
29
See:
30
30
31
31
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