Skip to content
This repository
Browse code

One more pass through zero sum to make sure everything works

  • Loading branch information...
commit 3e81df446e08e04cea92cbcd27eca35e6ea06e17 1 parent 862c898
Aaron Bedra abedra authored

Showing 1 changed file with 96 additions and 85 deletions. Show diff stats Hide diff stats

  1. +96 85 src/labs/zero_sum.clj
181 src/labs/zero_sum.clj
@@ -8,133 +8,144 @@
8 8
9 9 (defn overview []
10 10 [[:h3 "Overview"]
11   - [:p "This lab will demonstrate using Clojure's Software Transactional Memory (STM), and get you
12   - started thinking about where identities occur in your code."]])
  11 + [:p "This lab will demonstrate using Clojure's Software Transactional Memory (STM),
  12 + and get you started thinking about where identities occur in your code."]])
13 13
14 14 (defn requirements []
15 15 [[:h3 "Requirements"]
16   - [:p "You will be tracking the allocation of a fixed resource shared by N named accounts. A working
17   - solution will have the following:"]
  16 + [:p "You will be tracking the allocation of a fixed resource shared by N named
  17 + accounts. A working solution will have the following:"]
18 18 [:ol
19 19 [:li "A function to create an initial set of account names and balances."]
20 20 [:li "A function that transfers from one account to another."]
21 21 [:li "A function to get the current balance for an individual account."]
22   - [:li "A function to get the total balance across all accounts. This balance should never change,
23   - since the resource is fixed and transfers are zero-sum."]
  22 + [:li "A function to get the total balance across all accounts. This balance should
  23 + never change, since the resource is fixed and transfers are zero-sum."]
24 24 [:li "All functions should be threadsafe!"]]
25   - [:p "If you are already somewhat comfortable with Clojure's reference API, try implementing these
26   - requirements from scratch. Otherwise, or if you get stuck, you can refer to the step-by-step
27   - instructions that follow."]])
  25 + [:p "If you are already somewhat comfortable with Clojure's reference API, try
  26 + implementing these requirements from scratch. Otherwise, or if you get stuck,
  27 + you can refer to the step-by-step instructions that follow."]])
28 28
29 29 (defn simple-accounts []
30 30 [[:h3 "Simple Accounts"]
31 31 [:ol
32   - [:li "Create a function named " (c make-accounts) " that takes a map with " (c :count) " and "
33   - (c :initial-balance) ". It should create a map whose keys are names (using numbers from
34   - 0 to count-1) and whose values are refs to the initial balance."
  32 + [:li "Create a function named " (c make-accounts) " that takes a map with "
  33 + (c :count) " and " (c :initial-balance) ". It should create a map whose
  34 + keys are names (using numbers from 0 to count-1) and whose values are
  35 + refs to the initial balance."
35 36 (showme accounts-1/make-accounts)]
36   - [:li "Create a function named " (c total-balance) " that sums account values for accounts created
37   - by " (c :make-accounts) ". You will need to dereference each value in the map."
38   - (showme accounts-1/total-balance)]
39   - [:li "Create a " (c transfer) " function that takes a map keyed with " (c [:accounts :from :to :amount]) "
40   - and does a transfer. You will need to use " (c dosync) " to scope a transaction, and you should "
41   - [:i "not"] " need to actually read any balances."
42   - (showme accounts-1/transfer)]
43   - [:li "Create a " (c balance) " function that takes " (c accounts) " and an " (c account-id) " and
44   - returns the current balance for an account."
  37 + [:li "Create a function named " (c total-balance) " that sums account values for
  38 + accounts created by " (c :make-accounts) ". You will need to dereference
  39 + each value in the map." (showme accounts-1/total-balance)]
  40 + [:li "Create a " (c transfer) " function that takes a map keyed with "
  41 + (c [:accounts :from :to :amount]) " and does a transfer. You will need to
  42 + use " (c dosync) " to scope a transaction, and you should " [:i "not"] "
  43 + need to actually read any balances." (showme accounts-1/transfer)]
  44 + [:li "Create a " (c balance) " function that takes " (c accounts) " and an "
  45 + (c account-id) " and returns the current balance for an account."
45 46 (showme accounts-1/balance)]
46   - [:li "Using the REPL or a unit testing framework you already know to test these functions against
47   - the requirements listed at the start of the lab. Don't worry about the \"must be thread safe!\"
48   - requirement yet."]]])
  47 + [:li "Using the REPL or a unit testing framework you already know to test these
  48 + functions against the requirements listed at the start of the lab. Don't
  49 + worry about the \"must be thread safe!\" requirement yet."]]])
49 50
50 51 (defn is-it-threadsafe []
51 52 [[:h3 "Is It Threadsafe?"]
52 53 [:p "No."]
53   - [:p "The transaction guarantees that all updates are atomic, consistent, and isolated. But the reads
54   - are not atomic, so " (c total-balance) " could be wrong. Let's prove it in code, by generating
55   - a bunch of random transactions on multiple threads and then watching reads get out of sync."]
  54 + [:p "The transaction guarantees that all updates are atomic, consistent, and
  55 + isolated. But the reads are not atomic, so " (c total-balance) " could be
  56 + wrong. Let's prove it in code, by generating a bunch of random transactions
  57 + on multiple threads and then watching reads get out of sync."]
56 58 [:ol
57   - [:li "First, we need some random account ids: a " (c from-id) " and a " (c to-id) ". But in Clojure
58   - it rarely makes sense to deliver " [:i "two"] " of something, when you could deliver a lazy,
59   - infinite sequence instead. Write a " (c random-account-ids) " function that returns a lazy
60   - sequence of ids from accounts. You can use " (c rand-nth) " to get a random element from a
61   - collection, and you can use " (c repeatedly) " to return a lazy sequence of results from calling
62   - an impure function."
  59 + [:li "First, we need some random account ids: a " (c from-id) " and a " (c to-id) ".
  60 + But in Clojure it rarely makes sense to deliver " [:i "two"] " of something,
  61 + when you could deliver a lazy, infinite sequence instead. Write a "
  62 + (c random-account-ids) " function that returns a lazy sequence of ids from
  63 + accounts. You can use " (c rand-nth) " to get a random element from a
  64 + collection, and you can use " (c repeatedly) " to return a lazy sequence of
  65 + results from calling an impure function."
63 66 (showme accounts-1/random-account-ids)]
64   - [:li "Now, create a " (c random-transfer) " function to do a transfer at random. Let "
65   - (c [from-id to-id]) " from " (c random-account-ids) ", then use " (c rand-int) " to let a
66   - random transfer amount, then make the " (c transfer) " :"
67   - (showme accounts-1/random-transfer)]
  67 + [:li "Now, create a " (c random-transfer) " function to do a transfer at random.
  68 + Let " (c [from-id to-id]) " from " (c random-account-ids) ", then use "
  69 + (c rand-int) " to let a random transfer amount, then make the "
  70 + (c transfer) " :" (showme accounts-1/random-transfer)]
68 71 [:li "Next we need to do a bunch of transfers, from multiple threads."
69   - [:ul "Create a " (c bunch-o-txes) " function that takes " (c [accounts n iterations]) "."]
  72 + [:ul "Create a " (c bunch-o-txes) " function that takes "
  73 + (c [accounts n iterations]) "."]
70 74 [:ul "Use " (c dotimes) " to do a fraction of the iterations on each thread."]
71 75 [:ul "Use " (c future) " to package the work to run on another thread."]
72 76 [:ul "Use " (c take) " and " (c repeatedly) " to get n threads worth."]
73 77 (showme accounts-1/bunch-o-txes)]
74 78 [:li "Finally, we need a " (c trial) " function that puts it all together."
75 79 [:ul "Trial should take a map with keys " (c [:accounts :iterations :threads]) ". "]
76   - [:ul "Let a variable to track the" (c expected-balance) " (which is the initial balance of accounts)."]
  80 + [:ul "Let a variable to track the" (c expected-balance) " (which is the initial
  81 + balance of accounts)."]
77 82 [:ul "Let a variable to hold some futures created by calling " (c bunch-o-txes) ". "]
78   - [:ul "Start a loop. If all the futures are " (c (.isDone f)) ", then return the accounts and futures in
79   - a map. (This is useful for poking at the results in the REPL)."]
80   - [:ul "If the futures are not done, assert that the " (c expected-balance) " is still right,
81   - and " (c recur) ". "]
82   - [:ul "Create a no-argument arity version that starts with 10 accounts, balance 100 each, 1000
83   - iterations, and 2 threads."]
  83 + [:ul "Start a loop. If all the futures are " (c (.isDone f)) ", then return the
  84 + accounts and futures in a map. (This is useful for poking at the results in
  85 + the REPL)."]
  86 + [:ul "If the futures are not done, assert that the " (c expected-balance) " is
  87 + still right, and " (c recur) ". "]
  88 + [:ul "Create a no-argument arity version that starts with 10 accounts, balance
  89 + 100 each, 1000 iterations, and 2 threads."]
84 90 (showme accounts-1/trial)]
85   - [:li "Try running the trial. In my tests on a dual-core machine, the assertion fails almost instantly"]
  91 + [:li "Try running the trial. In my tests on a dual-core machine, the assertion
  92 + fails almost instantly"]
86 93 [:li "A simple fix is to change " (c total-balance) " to read within a transaction,
87 94 which guarantees that all reads are from the same point-in-time."
88 95 (showme accounts-2/total-balance)
89   - "Now you should be able to run as long a " (c trial) " as you like without a problem."]]
90   - [:p "Reading from a transaction is fast, requires no locks, and never impedes writers. However,
91   - there is a way to solve this problem that avoids the read transaction, by changing the
92   - granularity we use to think about identity."]])
  96 + "Now you should be able to run as long a " (c trial) " as you like without a
  97 + problem."]]
  98 + [:p "Reading from a transaction is fast, requires no locks, and never impedes
  99 + writers. However, there is a way to solve this problem that avoids the read
  100 + transaction, by changing the granularity we use to think about identity."]])
93 101
94 102 (defn granularity []
95 103 [[:h3 "Granularity of Identity"]
96   - [:p "So far, the unit of identity has been the individual account. But what if our unit of
97   - identity was the set of all accounts? Instead of a map with reference values, we could
98   - use a single reference to an immutable map. Let's make this change, and see how it affects
99   - each of our functions:"]
  104 + [:p "So far, the unit of identity has been the individual account. But what if our
  105 + unit of identity was the set of all accounts? Instead of a map with reference
  106 + values, we could use a single reference to an immutable map. Let's make this
  107 + change, and see how it affects each of our functions:"]
100 108 [:ol
101   - [:li "The new version of " (c make-accounts) " should be a ref to a map, not a map whose values are refs:"
102   - (showme accounts-3/make-accounts)]
103   - [:li "The new version of " (c total-balance) " is simpler: it merely sumes the values of the referenced "
104   - (c accounts) " map:"
105   - (showme accounts-3/total-balance)]
106   - [:li "The new version of " (c transfer) " is more complex. Inside the transaction, you need to make two
107   - updates to the same map. Hint: use the " (c update-in) " function:"
108   - (showme accounts-3/transfer)]
109   - [:li "The new " (c balance) " function differs only in placement of parentheses, so that the dereference
110   - comes before the key lookup:"
  109 + [:li "The new version of " (c make-accounts) " should be a ref to a map, not a map
  110 + whose values are refs:" (showme accounts-3/make-accounts)]
  111 + [:li "The new version of " (c total-balance) " is simpler: it merely sumes the
  112 + values of the referenced " (c accounts) " map:"
  113 + (showme accounts-3/total-balance)]
  114 + [:li "The new version of " (c transfer) " is more complex. Inside the transaction,
  115 + you need to make two updates to the same map. Hint: use the " (c update-in) "
  116 + function:" (showme accounts-3/transfer)]
  117 + [:li "The new " (c balance) " function differs only in placement of parentheses,
  118 + so that the dereference comes before the key lookup:"
111 119 (showme accounts-3/balance)]
112   - [:li "The " (c random-account-ids) " function now needs to dereference " (c accounts) ":"
113   - (showme accounts-3/random-account-ids)]
114   - [:li "That's it. The driver functions that you use to test transactions do not have to change. Verify
115   - that the new system works correctly."]]])
  120 + [:li "The " (c random-account-ids) " function now needs to dereference "
  121 + (c accounts) ":" (showme accounts-3/random-account-ids)]
  122 + [:li "That's it. The driver functions that you use to test transactions do not
  123 + have to change. Verify that the new system works correctly."]]])
116 124
117 125 (defn bonus []
118 126 [[:h3 "Bonus"]
119 127 [:ol
120   - [:li "In the code above, you experimented with two different identities: the account and the set of
121   - all accounts. One of these approaches is the correct one. Which one, and why? Under what
122   - circumstances would the other approach make sense?"]
123   - [:li "The identity-per-account version of " (c make-accounts) " used " (c repeatedly) ", but the
124   - identity-for-all-accounts version used " (c repeat) ". Why the difference?"]
125   - [:li "Don't let the terminology of the example trap you into dollars-and-cents thinking. If you
126   - were building a program to play Monopoly, how many places might you use this code?"]
127   - [:li "What does the solution do to prevent accounts from going negative? How reliable is this?
  128 + [:li "In the code above, you experimented with two different identities: the
  129 + account and the set of all accounts. One of these approaches is the correct
  130 + one. Which one, and why? Under what circumstances would the other approach
  131 + make sense?"]
  132 + [:li "The identity-per-account version of " (c make-accounts) " used "
  133 + (c repeatedly) ", but the identity-for-all-accounts version used "
  134 + (c repeat) ". Why the difference?"]
  135 + [:li "Don't let the terminology of the example trap you into dollars-and-cents
  136 + thinking. If you were building a program to play Monopoly, how many places
  137 + might you use this code?"]
  138 + [:li "What does the solution do to prevent accounts from going negative? How
  139 + reliable is this?
128 140 Sketch out at least two other approaches."]
129 141 [:li "How is reading in a transaction different from reading in a lock?"]
130   - [:li "Could you use " (c commute) " instead of " (c alter) " to update the account balances?
131   - Read the requirements carefully."]]])
  142 + [:li "Could you use " (c commute) " instead of " (c alter) " to update the account
  143 + balances? Read the requirements carefully."]]])
132 144
133 145 (defn instructions []
134   - (concat
135   - (overview)
136   - (requirements)
137   - (simple-accounts)
138   - (is-it-threadsafe)
139   - (granularity)
140   - (bonus)))
  146 + (concat (overview)
  147 + (requirements)
  148 + (simple-accounts)
  149 + (is-it-threadsafe)
  150 + (granularity)
  151 + (bonus)))

0 comments on commit 3e81df4

Please sign in to comment.
Something went wrong with that request. Please try again.