optimism.texi 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. @node Optimistic concurrency
  2. @section Optimistic concurrency
  3. Scheme48's fundamental thread synchronization mechanism is based on a
  4. device often used in high-performance database systems: optimistic
  5. concurrency. The basic principle of optimistic concurrency is that,
  6. rather than mutually excluding other threads from data involved in one
  7. thread's transaction, a thread keeps a log of its transaction, not
  8. actually modifying the data involved, only touching the log. When the
  9. thread is ready to commit its changes, it checks that all of the reads
  10. from memory retained their integrity --- that is, all of the memory
  11. that was read from during the transaction has remained the same, and is
  12. consistent with what is there at the time of the commit. If, and only
  13. if, all of the reads remained valid, the logged writes are committed;
  14. otherwise, the transaction has been invalidated. While a thread is
  15. transacting, any number of other threads may be also transacting on the
  16. same resource. All that matters is that the values each transaction
  17. read are consistent with every write that was committed during the
  18. transaction. This synchronization mechanism allows for wait-free,
  19. lockless systems that easily avoid confusing problems involving careful
  20. sequences of readily deadlock-prone mutual exclusion.
  21. @cindex proposals
  22. @cindex logs
  23. @cindex transaction logs
  24. @cindex optimistic concurrency proposals
  25. @cindex optimistic concurrency logs
  26. In the Scheme48 system, every thread has its own log of transactions,
  27. called a @dfn{proposal}. There are variants of all data accessors &
  28. modifiers that operate on the current thread's proposal, rather than
  29. actual memory: after the initial read of a certain part of memory ---
  30. which @emph{does} perform a real read ---, the value from that location
  31. in memory is cached in the proposal, and thenceforth reads from that
  32. location in memory will actually read the cache; modifications touch
  33. only the proposal, until the proposal is committed.
  34. @stindex proposals
  35. All of the names described in this section are exported by the
  36. @code{proposals} structure.
  37. @subsection High-level optimistic concurrency
  38. @cindex atomic regions
  39. There are several high-level operations that abstract the manipulation
  40. of the current thread's proposal.
  41. @deffn procedure call-ensuring-atomicity thunk @returns{} values
  42. @deffnx procedure call-ensuring-atomicity! thunk @returns{} unspecified
  43. These ensure that the operation of @var{thunk} is atomic. If there is
  44. already a current proposal in place, these are equivalent to calling
  45. @var{thunk}. If there is not a current proposal in place, these
  46. install a new proposal, call @var{thunk}, and attempt to commit the new
  47. proposal. If the commit succeeded, these return. If it failed, these
  48. retry with a new proposal until they do succeed.
  49. @code{Call-ensuring-atomicity} returns the values that @var{thunk}
  50. returned when the commit succeeded; @code{call-ensuring-atomicity!}
  51. returns zero values --- it is intended for when @var{thunk} is used for
  52. its effects only.
  53. @end deffn
  54. @deffn procedure call-atomically thunk @returns{} values
  55. @deffnx procedure call-atomically! thunk @returns{} unspecified
  56. These are like @var{call-ensuring-atomicity} and
  57. @var{call-ensuring-atomicity!}, respectively, except that they always
  58. install a new proposal (saving the old one and restoring it when they
  59. are done).
  60. @end deffn
  61. @deffn syntax ensure-atomicity body @returns{} values
  62. @deffnx syntax ensure-atomicity! body @returns{} unspecified
  63. @deffnx syntax atomically body @returns{} values
  64. @deffnx syntax atomically! body @returns{} unspecified
  65. These are syntactic sugar over @code{call-ensuring-atomicity},
  66. @code{call-ensuring-atomicity!}, @code{call-atomically}, and
  67. @code{call-atomically!}, respectively.
  68. @end deffn
  69. Use these high-level optimistic concurrency operations to make the
  70. body atomic. @code{Call-ensuring-atomicity} @etc{} simply ensure that
  71. the transaction will be atomic, and may `fuse' it with an enclosing
  72. atomic transaction if there already is one, @ie{} use the proposal for
  73. that transaction already in place, creating one only if there is not
  74. already one. @code{Call-atomically} @etc{} are for what might be
  75. called `subatomic' transactions, which cannot be fused with other
  76. atomic transactions, and for which there is always created a new
  77. proposal.
  78. However, code within @code{call-ensuring-atomicity} @etc{} or
  79. @code{call-atomically} @etc{} should @emph{not} explicitly commit the
  80. current proposal; those operations above @emph{automatically} commit
  81. the current proposal when the atomic transaction is completed. (In
  82. the case of @code{call-atomically} @etc{}, this is when the procedure
  83. passed returns; in the case of @code{call-ensuring-atomicity} @etc{},
  84. this is when the outermost enclosing atomic transaction completes, or
  85. the same as @code{call-atomically} if there was no enclosing atomic
  86. transaction.) To explicitly commit the current proposal --- for
  87. example, to perform some particular action if the commit fails rather
  88. than just to repeatedly retry the transaction, or to use operations
  89. from the @embedref{Custom thread synchronization, customized thread
  90. synchronization} facilities that commit the current proposal after
  91. their regular function, or the operations on @embedref{Higher-level
  92. synchronization, condition variables} that operate on the condition
  93. variable and then commit the current proposal ---, one must use the
  94. @code{with-new-proposal} syntax as described below, not these
  95. operations.
  96. @subsection Logging variants of Scheme procedures
  97. @cindex logging operations
  98. @cindex optimistic concurrency logging operations
  99. @deffn procedure provisional-car pair @returns{} value
  100. @deffnx procedure provisional-cdr pair @returns{} value
  101. @deffnx procedure provisional-set-car! pair value @returns{} unspecified
  102. @deffnx procedure provisional-set-cdr! pair value @returns{} unspecified
  103. @deffnx procedure provisional-cell-ref cell @returns{} value
  104. @deffnx procedure provisional-cell-set! cell value @returns{} unspecified
  105. @deffnx procedure provisional-vector-ref vector index @returns{} value
  106. @deffnx procedure provisional-vector-set! vector index value @returns{} unspecified
  107. @deffnx procedure provisional-string-ref string index @returns{} char
  108. @deffnx procedure provisional-string-set! string index value @returns{} unspecified
  109. @deffnx procedure provisional-byte-vector-ref byte-vector index @returns{} char
  110. @deffnx procedure provisional-byte-vector-set! byte-vector index byte @returns{} unspecified
  111. @deffnx procedure attempt-copy-bytes! from fstart to tstart count @returns{} unspecified
  112. These are variants of most basic Scheme memory accessors & modifiers
  113. that log in the current proposal, rather than performing the actual
  114. memory access/modification. All of these do perform the actual memory
  115. access/modification, however, if there is no current proposal in place
  116. when they are called. @code{Attempt-copy-bytes!} copies a sequence of
  117. @var{count} bytes from the byte vector or string @var{from}, starting
  118. at the index @var{fstart}, to the byte vector or string @var{to},
  119. starting at the index @var{tstart}.
  120. @end deffn
  121. @subsection Synchronized records
  122. @cindex optimistically concurrent record types
  123. @deffn syntax define-synchronized-record-type
  124. @lisp
  125. (define-synchronized-record-type @var{tag} @var{type-name}
  126. (@var{constructor-name} @var{parameter-field-tag} @dots{})
  127. [(@var{sync-field-tag} @dots{})]
  128. @var{predicate-name}
  129. (@var{field-tag} @var{accessor-name} [@var{modifier-name}])
  130. @dots{})@end lisp
  131. This is exactly like @code{define-record-type} from the
  132. @code{define-record-types} structure, except that the accessors &
  133. modifiers for each field in @var{sync-field-tag} @dots{} are defined to
  134. be provisional, @ie{} to log in the current proposal. If the list of
  135. synchronized fields is absent, all of the fields are synchronized,
  136. @ie{} it is as if all were specified in that list.
  137. @end deffn
  138. @stindex define-sync-record-types
  139. The @code{proposals} structure also exports
  140. @code{define-record-discloser} (@pxref{Records}). Moreover, the
  141. @code{define-sync-record-types} structure, too, exports
  142. @code{define-synchronized-record-type}, though it does not export
  143. @code{define-record-discloser}.
  144. @subsection Optimistic concurrency example
  145. Here is a basic example of using optimistic concurrency to ensure the
  146. synchronization of memory. We first present a simple mechanism for
  147. counting integers by maintaining internal state, which is expressed
  148. easily with closures:
  149. @lisp
  150. (define (make-counter value)
  151. (lambda ()
  152. (let ((v value))
  153. (set! value (+ v 1))
  154. v)))@end lisp
  155. This has a problem: between obtaining the value of the closure's slot
  156. for @code{value} and updating that slot, another thread might be given
  157. control and modify the counter, producing unpredictable results in
  158. threads in the middle of working with the counter. To remedy this, we
  159. might add a mutual exclusion lock to counters to prevent threads from
  160. simultaneously accessing the cell:
  161. @lisp
  162. (define (make-counter value)
  163. (let ((lock (make-lock)))
  164. (lambda ()
  165. (dynamic-wind
  166. (lambda () (obtain-lock lock))
  167. (lambda ()
  168. (let ((v value))
  169. (set! value (+ v 1))
  170. v))
  171. (lambda () (release-lock lock))))))@end lisp
  172. This poses another problem, however. Suppose we wish to write an
  173. atomic @code{(step-counters! @var{counter @dots{}})} procedure that
  174. increments each of the supplied counters by one; supplying a counter
  175. @var{n} times should have the effect of incrementing it by @var{n}.
  176. The na@"{@dotless{i}}ve definition of it is this:
  177. @lisp
  178. (define (step-counters! . counters)
  179. (for-each (lambda (counter) (counter))
  180. counters))@end lisp
  181. Obviously, though, this is not atomic, because each individual counter
  182. is locked when it is used, but not the whole iteration across them.
  183. To work around this, we might use an obfuscated control structure to
  184. allow nesting the locking of counters:
  185. @lisp
  186. (define (make-counter value)
  187. (let ((lock (make-lock)))
  188. (lambda args
  189. (dynamic-wind
  190. (lambda () (obtain-lock lock))
  191. (lambda ()
  192. (if (null? args)
  193. (let ((v value))
  194. (set! value (+ v 1))
  195. v)
  196. ((car args))))
  197. (lambda () (release-lock lock))))))
  198. (define (step-counters! . counters)
  199. (let loop ((cs counters))
  200. (if (null? cs)
  201. (for-each (lambda (counter) (counter))
  202. counters)
  203. ((car cs) (lambda () (loop (cdr cs)))))))@end lisp
  204. Aside from the obvious matter of the obfuscation of the control
  205. structures used here, however, this has another problem: we cannot
  206. step one counter multiple times atomically. Though different locks
  207. can be nested, nesting is very dangerous, because accidentally
  208. obtaining a lock that is already obtained can cause deadlock, and
  209. there is no modular, transparent way to avoid this in the general
  210. case.
  211. Instead, we can implement counters using optimistic concurrency to
  212. synchronize the shared data. The state of counters is kept explicitly
  213. in a @embedref{Cells, cell}, in order to use a provisional accessor &
  214. modifier, as is necessary to make use of optimistic concurrency, and
  215. we surround with @code{call-ensuring-atomicity} any regions we wish to
  216. be atomic:
  217. @lisp
  218. (define (make-counter initial)
  219. (let ((cell (make-cell initial)))
  220. (lambda ()
  221. (call-ensuring-atomicity
  222. (lambda ()
  223. (let ((value (provisional-cell-ref cell)))
  224. (provisional-cell-set! cell (+ value 1))
  225. value))))))
  226. (define (step-counters! . counters)
  227. (call-ensuring-atomicity!
  228. (lambda ()
  229. (for-each (lambda (counter) (counter))
  230. counters))))@end lisp
  231. @noindent
  232. This approach has a number of advantages:
  233. @itemize @bullet
  234. @item The original control structure is preserved, only with
  235. provisional operators for shared memory access that we explicitly wish
  236. to be synchronized and with @code{call-ensuring-atomicity} wrapping
  237. the portions of code that we explicitly want to be atomic.
  238. @item Composition of transactions is entirely transparent; it is
  239. accomplished automatically simply by @code{call-ensuring-atomicity}.
  240. @item Transactions can be nested arbitrarily deeply, and there is no
  241. problem of accidentally locking the same resource again at a deeper
  242. nesting level to induce deadlock.
  243. @item No explicit mutual exclusion or blocking is necessary. Threads
  244. proceed without heed to others, but do not actually write data to the
  245. shared memory until its validity is ensured. There is no deadlock at
  246. all.
  247. @end itemize
  248. @subsection Low-level optimistic concurrency
  249. Along with the higher-level operations described above, there are some
  250. lower-level primitives for finer control over optimistic concurrency.
  251. @cindex proposals
  252. @deffn procedure make-proposal @returns{} proposal
  253. @deffnx procedure current-proposal @returns{} proposal
  254. @deffnx procedure set-current-proposal! proposal @returns{} unspecified
  255. @deffnx procedure remove-current-proposal! @returns{} unspecified
  256. @code{Make-proposal} creates a fresh proposal. @code{Current-proposal}
  257. returns the current thread's proposal. @code{Set-current-proposal!}
  258. sets the current thread's proposal to @var{proposal}.
  259. @code{Remove-current-proposal!} sets the current thread's proposal to
  260. @code{#f}.
  261. @end deffn
  262. @cindex committing proposals
  263. @cindex proposals, committing
  264. @deffn procedure maybe-commit @returns{} boolean
  265. @deffnx procedure invalidate-current-proposal! @returns{} unspecified
  266. @code{Maybe-commit} checks that the current thread's proposal is still
  267. valid. If it is, the proposal's writes are committed, and
  268. @code{maybe-commit} returns @code{#t}; if not, the current thread's
  269. proposal is set to @code{#f} and @code{maybe-commit} returns @code{#f}.
  270. @code{Invalidate-current-proposal!} causes an inconsistency in the
  271. current proposal by caching a read and then directly writing to the
  272. place that read was from.
  273. @end deffn
  274. @cindex installing proposals
  275. @cindex proposals, installing
  276. @deffn syntax with-new-proposal (lose) body @returns{} values
  277. Convenience for repeating a transaction. @code{With-new-proposal}
  278. saves the current proposal and will reinstates it when everything is
  279. finished. After saving the current proposal, it binds @var{lose} to a
  280. nullary procedure that installs a fresh proposal and that evaluates
  281. @var{body}; it then calls @var{lose}. Typically, the last thing, or
  282. close to last thing, that @var{body} will do is attempt to commit the
  283. current proposal, and, if that fails, call @var{lose} to retry.
  284. @code{With-new-proposal} expands to a form that returns the values
  285. that @var{body} returns.
  286. This @code{retry-at-most} example tries running the transaction of
  287. @var{thunk}, and, if it fails to commit, retries at most @var{n}
  288. times. If the transaction is successfully committed before @var{n}
  289. repeated attempts, it returns true; otherwise, it returns false.
  290. @lisp
  291. (define (retry-at-most n thunk)
  292. (with-new-proposal (lose)
  293. (thunk)
  294. (cond ((maybe-commit) #t)
  295. ((zero? n) #f)
  296. (else (set! n (- n 1))
  297. (lose)))))@end lisp
  298. @end deffn